Added EvalSession way of controlling all coroutines started from a script
This commit is contained in:
parent
446c8d9a6e
commit
fc01016a74
53
README.md
53
README.md
@ -93,42 +93,49 @@ import net.sergeych.lyng.*
|
|||||||
// we need a coroutine to start, as Lyng
|
// we need a coroutine to start, as Lyng
|
||||||
// is a coroutine based language, async topdown
|
// is a coroutine based language, async topdown
|
||||||
runBlocking {
|
runBlocking {
|
||||||
assert(5 == eval(""" 3*3 - 4 """).toInt())
|
val session = EvalSession()
|
||||||
eval(""" println("Hello, Lyng!") """)
|
assert(5 == session.eval(""" 3*3 - 4 """).toInt())
|
||||||
|
session.eval(""" println("Hello, Lyng!") """)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Exchanging information
|
### Exchanging information
|
||||||
|
|
||||||
Script is executed over some `Scope`. Create instance,
|
The preferred host runtime is `EvalSession`. It owns the script scope and any coroutines
|
||||||
add your specific vars and functions to it, and call:
|
started with `launch { ... }`. Create a session, grab its scope when you need low-level
|
||||||
|
binding APIs, then execute scripts through the session:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import net.sergeych.lyng.*
|
import net.sergeych.lyng.*
|
||||||
|
|
||||||
// simple function
|
runBlocking {
|
||||||
val scope = Script.newScope().apply {
|
val session = EvalSession()
|
||||||
addFn("sumOf") {
|
val scope = session.getScope().apply {
|
||||||
var sum = 0.0
|
// simple function
|
||||||
for (a in args) sum += a.toDouble()
|
addFn("sumOf") {
|
||||||
ObjReal(sum)
|
var sum = 0.0
|
||||||
}
|
for (a in args) sum += a.toDouble()
|
||||||
addConst("LIGHT_SPEED", ObjReal(299_792_458.0))
|
ObjReal(sum)
|
||||||
|
}
|
||||||
|
addConst("LIGHT_SPEED", ObjReal(299_792_458.0))
|
||||||
|
|
||||||
// callback back to kotlin to some suspend fn, for example::
|
// callback back to kotlin to some suspend fn, for example::
|
||||||
// suspend fun doSomeWork(text: String): Int
|
// suspend fun doSomeWork(text: String): Int
|
||||||
addFn("doSomeWork") {
|
addFn("doSomeWork") {
|
||||||
// this _is_ a suspend lambda, we can call suspend function,
|
// this _is_ a suspend lambda, we can call suspend function,
|
||||||
// and it won't consume the thread.
|
// and it won't consume the thread.
|
||||||
// note that in kotlin handler, `args` is a list of `Obj` arguments
|
// note that in kotlin handler, `args` is a list of `Obj` arguments
|
||||||
// and return value from this lambda should be Obj too:
|
// and return value from this lambda should be Obj too:
|
||||||
doSomeWork(args[0]).toObj()
|
doSomeWork(args[0]).toObj()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// execute through the session:
|
||||||
|
session.eval("sumOf(1,2,3)") // <- 6
|
||||||
}
|
}
|
||||||
// adding constant:
|
|
||||||
scope.eval("sumOf(1,2,3)") // <- 6
|
|
||||||
```
|
```
|
||||||
Note that the scope stores all changes in it so you can make calls on a single scope to preserve state between calls.
|
Note that the session reuses one scope, so state persists across `session.eval(...)` calls.
|
||||||
|
Use raw `Scope.eval(...)` only when you intentionally want low-level control without session-owned coroutine lifecycle.
|
||||||
|
|
||||||
## IntelliJ IDEA plugin: Lightweight autocompletion (experimental)
|
## IntelliJ IDEA plugin: Lightweight autocompletion (experimental)
|
||||||
|
|
||||||
|
|||||||
@ -80,11 +80,11 @@ If you already have an ionspin `BigDecimal` on the host side, the simplest suppo
|
|||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.EvalSession
|
||||||
import net.sergeych.lyng.asFacade
|
import net.sergeych.lyng.asFacade
|
||||||
import net.sergeych.lyng.newDecimal
|
import net.sergeych.lyng.newDecimal
|
||||||
|
|
||||||
val scope = Script.newScope()
|
val scope = EvalSession().getScope()
|
||||||
val decimal = scope.asFacade().newDecimal(BigDecimal.parseStringWithMode("12.34"))
|
val decimal = scope.asFacade().newDecimal(BigDecimal.parseStringWithMode("12.34"))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -36,21 +36,60 @@ dependencies {
|
|||||||
|
|
||||||
If you use Kotlin Multiplatform, add the dependency in the `commonMain` source set (and platform‑specific sets if you need platform APIs).
|
If you use Kotlin Multiplatform, add the dependency in the `commonMain` source set (and platform‑specific sets if you need platform APIs).
|
||||||
|
|
||||||
### 2) Create a runtime (Scope) and execute scripts
|
### 2) Preferred runtime: `EvalSession`
|
||||||
|
|
||||||
The easiest way to get a ready‑to‑use scope with standard packages is via `Script.newScope()`.
|
For host applications, prefer `EvalSession` as the main way to run scripts.
|
||||||
|
It owns one reusable Lyng scope, serializes `eval(...)` calls, and governs coroutines started from Lyng `launch { ... }`.
|
||||||
|
|
||||||
|
Main entrypoints:
|
||||||
|
|
||||||
|
- `session.eval(code)` / `session.eval(source)`
|
||||||
|
- `session.getScope()` when you need low-level binding APIs
|
||||||
|
- `session.cancel()` to cancel active session-owned coroutines
|
||||||
|
- `session.join()` to wait for active session-owned coroutines
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun main() = kotlinx.coroutines.runBlocking {
|
||||||
|
val session = EvalSession()
|
||||||
|
|
||||||
|
// Evaluate a one‑liner
|
||||||
|
val result = session.eval("1 + 2 * 3")
|
||||||
|
println("Lyng result: $result") // ObjReal/ObjInt etc.
|
||||||
|
|
||||||
|
// Optional lifecycle management
|
||||||
|
session.join()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The session creates its underlying scope lazily. If you need raw low-level APIs, get the scope explicitly:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `cancel()` / `join()` to govern async work started by scripts:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val session = EvalSession()
|
||||||
|
session.eval("""launch { delay(1000); println("done") }""")
|
||||||
|
session.cancel()
|
||||||
|
session.join()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1) Low-level runtime: `Scope`
|
||||||
|
|
||||||
|
Use `Scope` directly when you intentionally want lower-level control.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
fun main() = kotlinx.coroutines.runBlocking {
|
fun main() = kotlinx.coroutines.runBlocking {
|
||||||
val scope = Script.newScope() // suspends on first init
|
val scope = Script.newScope() // suspends on first init
|
||||||
|
|
||||||
// Evaluate a one‑liner
|
|
||||||
val result = scope.eval("1 + 2 * 3")
|
val result = scope.eval("1 + 2 * 3")
|
||||||
println("Lyng result: $result") // ObjReal/ObjInt etc.
|
println("Lyng result: $result")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also pre‑compile a script and execute it multiple times:
|
You can also pre‑compile a script and execute it multiple times on the same scope:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val script = Compiler.compile("""
|
val script = Compiler.compile("""
|
||||||
@ -63,7 +102,8 @@ val run1 = script.execute(scope)
|
|||||||
val run2 = script.execute(scope)
|
val run2 = script.execute(scope)
|
||||||
```
|
```
|
||||||
|
|
||||||
`Scope.eval("...")` is a shortcut that compiles and executes on the given scope.
|
`Scope.eval("...")` is the low-level shortcut that compiles and executes on the given scope.
|
||||||
|
For most embedding use cases, prefer `session.eval("...")`.
|
||||||
|
|
||||||
### 3) Preferred: bind extern globals from Kotlin
|
### 3) Preferred: bind extern globals from Kotlin
|
||||||
|
|
||||||
@ -85,6 +125,8 @@ import net.sergeych.lyng.bridge.*
|
|||||||
import net.sergeych.lyng.obj.ObjInt
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
import net.sergeych.lyng.obj.ObjString
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
val im = Script.defaultImportManager.copy()
|
val im = Script.defaultImportManager.copy()
|
||||||
im.addPackage("my.api") { module ->
|
im.addPackage("my.api") { module ->
|
||||||
module.eval("""
|
module.eval("""
|
||||||
@ -149,6 +191,9 @@ binder.bindGlobalFunRaw("echoRaw") { _, args ->
|
|||||||
Use this when you intentionally want raw `Scope` APIs. For most module APIs, prefer section 3.
|
Use this when you intentionally want raw `Scope` APIs. For most module APIs, prefer section 3.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
|
||||||
// A function returning value
|
// A function returning value
|
||||||
scope.addFn<ObjInt>("inc") {
|
scope.addFn<ObjInt>("inc") {
|
||||||
val x = args.firstAndOnly() as ObjInt
|
val x = args.firstAndOnly() as ObjInt
|
||||||
@ -167,7 +212,7 @@ scope.addVoidFn("log") {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// Call them from Lyng
|
// Call them from Lyng
|
||||||
scope.eval("val y = inc(41); log('Answer:', y)")
|
session.eval("val y = inc(41); log('Answer:', y)")
|
||||||
```
|
```
|
||||||
|
|
||||||
You can register multiple names (aliases) at once: `addFn<ObjInt>("inc", "increment") { ... }`.
|
You can register multiple names (aliases) at once: `addFn<ObjInt>("inc", "increment") { ... }`.
|
||||||
@ -254,6 +299,8 @@ If you want multi-axis slicing semantics, decode that list yourself in `getAt`.
|
|||||||
If you need a simple field (with a value) instead of a computed property, use `createField`. This adds a field to the class that will be present in all its instances.
|
If you need a simple field (with a value) instead of a computed property, use `createField`. This adds a field to the class that will be present in all its instances.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
val myClass = ObjClass("MyClass")
|
val myClass = ObjClass("MyClass")
|
||||||
|
|
||||||
// Add a read-only field (constant)
|
// Add a read-only field (constant)
|
||||||
@ -281,6 +328,8 @@ println(instance.count) // -> 5
|
|||||||
Properties in Lyng are pure accessors (getters and setters) and do not have automatic backing fields. You can add them to a class using `addProperty`.
|
Properties in Lyng are pure accessors (getters and setters) and do not have automatic backing fields. You can add them to a class using `addProperty`.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
val myClass = ObjClass("MyClass")
|
val myClass = ObjClass("MyClass")
|
||||||
var internalValue: Long = 10
|
var internalValue: Long = 10
|
||||||
|
|
||||||
@ -447,8 +496,9 @@ For Kotlin code that needs dynamic access to Lyng variables, functions, or membe
|
|||||||
It provides explicit, cached handles and predictable lookup rules.
|
It provides explicit, cached handles and predictable lookup rules.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val scope = Script.newScope()
|
val session = EvalSession()
|
||||||
scope.eval("""
|
val scope = session.getScope()
|
||||||
|
session.eval("""
|
||||||
val x = 40
|
val x = 40
|
||||||
fun add(a, b) = a + b
|
fun add(a, b) = a + b
|
||||||
class Box { var value = 1 }
|
class Box { var value = 1 }
|
||||||
@ -463,7 +513,7 @@ val x = resolver.resolveVal("x").get(scope)
|
|||||||
val sum = (resolver as BridgeCallByName).callByName(scope, "add", Arguments(ObjInt(1), ObjInt(2)))
|
val sum = (resolver as BridgeCallByName).callByName(scope, "add", Arguments(ObjInt(1), ObjInt(2)))
|
||||||
|
|
||||||
// Member access
|
// Member access
|
||||||
val box = scope.eval("Box()")
|
val box = session.eval("Box()")
|
||||||
val valueHandle = resolver.resolveMemberVar(box, "value")
|
val valueHandle = resolver.resolveMemberVar(box, "value")
|
||||||
valueHandle.set(scope, ObjInt(10))
|
valueHandle.set(scope, ObjInt(10))
|
||||||
val value = valueHandle.get(scope)
|
val value = valueHandle.get(scope)
|
||||||
@ -474,12 +524,14 @@ val value = valueHandle.get(scope)
|
|||||||
The simplest approach: evaluate an expression that yields the value and convert it.
|
The simplest approach: evaluate an expression that yields the value and convert it.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val kotlinAnswer = scope.eval("(1 + 2) * 3").toKotlin(scope) // -> 9 (Int)
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
val kotlinAnswer = session.eval("(1 + 2) * 3").toKotlin(scope) // -> 9 (Int)
|
||||||
|
|
||||||
// After scripts manipulate your vars:
|
// After scripts manipulate your vars:
|
||||||
scope.addOrUpdateItem("name", ObjString("Lyng"))
|
scope.addOrUpdateItem("name", ObjString("Lyng"))
|
||||||
scope.eval("name = name + ' rocks!'")
|
session.eval("name = name + ' rocks!'")
|
||||||
val kotlinName = scope.eval("name").toKotlin(scope) // -> "Lyng rocks!"
|
val kotlinName = session.eval("name").toKotlin(scope) // -> "Lyng rocks!"
|
||||||
```
|
```
|
||||||
|
|
||||||
Advanced: you can also grab a variable record directly via `scope.get(name)` and work with its `Obj` value, but evaluating `"name"` is often clearer and enforces Lyng semantics consistently.
|
Advanced: you can also grab a variable record directly via `scope.get(name)` and work with its `Obj` value, but evaluating `"name"` is often clearer and enforces Lyng semantics consistently.
|
||||||
@ -492,16 +544,20 @@ There are two convenient patterns.
|
|||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// Suppose Lyng defines: fun add(a, b) = a + b
|
// Suppose Lyng defines: fun add(a, b) = a + b
|
||||||
scope.eval("fun add(a, b) = a + b")
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
session.eval("fun add(a, b) = a + b")
|
||||||
|
|
||||||
val sum = scope.eval("add(20, 22)").toKotlin(scope) // -> 42
|
val sum = session.eval("add(20, 22)").toKotlin(scope) // -> 42
|
||||||
```
|
```
|
||||||
|
|
||||||
2) Call a Lyng function by name via a prepared call scope:
|
2) Call a Lyng function by name via a prepared call scope:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// Ensure the function exists in the scope
|
// Ensure the function exists in the scope
|
||||||
scope.eval("fun add(a, b) = a + b")
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
session.eval("fun add(a, b) = a + b")
|
||||||
|
|
||||||
// Look up the function object
|
// Look up the function object
|
||||||
val addFn = scope.get("add")!!.value as Statement
|
val addFn = scope.get("add")!!.value as Statement
|
||||||
@ -532,7 +588,8 @@ Register a Kotlin‑built package:
|
|||||||
import net.sergeych.lyng.bridge.*
|
import net.sergeych.lyng.bridge.*
|
||||||
import net.sergeych.lyng.obj.ObjInt
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
|
|
||||||
val scope = Script.newScope()
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
|
||||||
// Access the import manager behind this scope
|
// Access the import manager behind this scope
|
||||||
val im: ImportManager = scope.importManager
|
val im: ImportManager = scope.importManager
|
||||||
@ -563,12 +620,12 @@ im.addPackage("my.tools") { module: ModuleScope ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use it from Lyng
|
// Use it from Lyng
|
||||||
scope.eval("""
|
session.eval("""
|
||||||
import my.tools.*
|
import my.tools.*
|
||||||
val v = triple(14)
|
val v = triple(14)
|
||||||
status = "busy"
|
status = "busy"
|
||||||
""")
|
""")
|
||||||
val v = scope.eval("v").toKotlin(scope) // -> 42
|
val v = session.eval("v").toKotlin(scope) // -> 42
|
||||||
```
|
```
|
||||||
|
|
||||||
Register a package from Lyng source text:
|
Register a package from Lyng source text:
|
||||||
@ -582,24 +639,27 @@ val pkgText = """
|
|||||||
|
|
||||||
scope.importManager.addTextPackages(pkgText)
|
scope.importManager.addTextPackages(pkgText)
|
||||||
|
|
||||||
scope.eval("""
|
session.eval("""
|
||||||
import math.extra.*
|
import math.extra.*
|
||||||
val s = sqr(12)
|
val s = sqr(12)
|
||||||
""")
|
""")
|
||||||
val s = scope.eval("s").toKotlin(scope) // -> 144
|
val s = session.eval("s").toKotlin(scope) // -> 144
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also register from parsed `Source` instances via `addSourcePackages(source)`.
|
You can also register from parsed `Source` instances via `addSourcePackages(source)`.
|
||||||
|
|
||||||
### 10) Executing from files, security, and isolation
|
### 10) Executing from files, security, and isolation
|
||||||
|
|
||||||
- To run code from a file, read it and pass to `scope.eval(text)` or compile with `Compiler.compile(Source(fileName, text))`.
|
- To run code from a file, read it and pass to `session.eval(text)` or compile with `Compiler.compile(Source(fileName, text))`.
|
||||||
- `ImportManager` takes an optional `SecurityManager` if you need to restrict what packages or operations are available. By default, `Script.defaultImportManager` allows everything suitable for embedded use; clamp it down in sandboxed environments.
|
- `ImportManager` takes an optional `SecurityManager` if you need to restrict what packages or operations are available. By default, `Script.defaultImportManager` allows everything suitable for embedded use; clamp it down in sandboxed environments.
|
||||||
- For isolation, create fresh modules/scopes via `Scope.new()` or `Script.newScope()` when you need a clean environment per request.
|
- For isolation, prefer a fresh `EvalSession()` per request. Use `Scope.new()` / `Script.newScope()` when you specifically need low-level raw scopes or modules.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// Fresh module based on the default manager, without the standard prelude
|
// Preferred per-request runtime:
|
||||||
val isolated = net.sergeych.lyng.Scope.new()
|
val isolatedSession = EvalSession()
|
||||||
|
|
||||||
|
// Low-level fresh module based on the default manager, without the standard prelude:
|
||||||
|
val isolatedScope = net.sergeych.lyng.Scope.new()
|
||||||
```
|
```
|
||||||
|
|
||||||
### 11) Tips and troubleshooting
|
### 11) Tips and troubleshooting
|
||||||
@ -634,8 +694,11 @@ To simplify handling these objects from Kotlin, several extension methods are pr
|
|||||||
You can serialize Lyng exception objects using `Lynon` to transmit them across boundaries and then rethrow them.
|
You can serialize Lyng exception objects using `Lynon` to transmit them across boundaries and then rethrow them.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
scope.eval("throw MyUserException(404, \"Not Found\")")
|
session.eval("throw MyUserException(404, \"Not Found\")")
|
||||||
} catch (e: ExecutionError) {
|
} catch (e: ExecutionError) {
|
||||||
// 1. Serialize the Lyng exception object
|
// 1. Serialize the Lyng exception object
|
||||||
val encoded: UByteArray = lynonEncodeAny(scope, e.errorObject)
|
val encoded: UByteArray = lynonEncodeAny(scope, e.errorObject)
|
||||||
|
|||||||
@ -122,7 +122,8 @@ data class TestJson2(
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun deserializeMapWithJsonTest() = runTest {
|
fun deserializeMapWithJsonTest() = runTest {
|
||||||
val x = eval("""
|
val session = EvalSession()
|
||||||
|
val x = session.eval("""
|
||||||
import lyng.serialization
|
import lyng.serialization
|
||||||
{ value: 1, inner: { "foo": 1, "bar": 2 }}
|
{ value: 1, inner: { "foo": 1, "bar": 2 }}
|
||||||
""".trimIndent()).decodeSerializable<TestJson2>()
|
""".trimIndent()).decodeSerializable<TestJson2>()
|
||||||
@ -143,7 +144,8 @@ data class TestJson3(
|
|||||||
)
|
)
|
||||||
@Test
|
@Test
|
||||||
fun deserializeAnyMapWithJsonTest() = runTest {
|
fun deserializeAnyMapWithJsonTest() = runTest {
|
||||||
val x = eval("""
|
val session = EvalSession()
|
||||||
|
val x = session.eval("""
|
||||||
import lyng.serialization
|
import lyng.serialization
|
||||||
{ value: 12, inner: { "foo": 1, "bar": "two" }}
|
{ value: 12, inner: { "foo": 1, "bar": "two" }}
|
||||||
""".trimIndent()).decodeSerializable<TestJson3>()
|
""".trimIndent()).decodeSerializable<TestJson3>()
|
||||||
@ -175,4 +177,3 @@ on [Instant](time.md), see `Instant.truncateTo...` functions.
|
|||||||
|
|
||||||
(3)
|
(3)
|
||||||
: Map keys must be strings, map values may be any objects serializable to Json.
|
: Map keys must be strings, map values may be any objects serializable to Json.
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,13 @@
|
|||||||
#### Install in host
|
#### Install in host
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.EvalSession
|
||||||
import net.sergeych.lyng.io.console.createConsoleModule
|
import net.sergeych.lyng.io.console.createConsoleModule
|
||||||
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
||||||
|
|
||||||
suspend fun initScope() {
|
suspend fun initScope() {
|
||||||
val scope = Script.newScope()
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -39,23 +39,27 @@ This brings in:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Install the module into a Lyng Scope
|
#### Install the module into a Lyng session
|
||||||
|
|
||||||
The filesystem module is not installed automatically. You must explicitly register it in the scope’s `ImportManager` using the installer. You can customize access control via `FsAccessPolicy`.
|
The filesystem module is not installed automatically. The preferred host runtime is `EvalSession`: create the session, get its underlying scope, install the module there, and execute scripts through the session. You can customize access control via `FsAccessPolicy`.
|
||||||
|
|
||||||
Kotlin (host) bootstrap example:
|
Kotlin (host) bootstrap example:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
|
import net.sergeych.lyng.EvalSession
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.io.fs.createFs
|
import net.sergeych.lyng.io.fs.createFs
|
||||||
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
||||||
|
|
||||||
val scope: Scope = Scope.new()
|
suspend fun bootstrapFs() {
|
||||||
val installed: Boolean = createFs(PermitAllAccessPolicy, scope)
|
val session = EvalSession()
|
||||||
// installed == true on first registration in this ImportManager, false on repeats
|
val scope: Scope = session.getScope()
|
||||||
|
val installed: Boolean = createFs(PermitAllAccessPolicy, scope)
|
||||||
|
// installed == true on first registration in this ImportManager, false on repeats
|
||||||
|
|
||||||
// In scripts (or via scope.eval), import the module to use its symbols:
|
// In scripts (or via session.eval), import the module to use its symbols:
|
||||||
scope.eval("import lyng.io.fs")
|
session.eval("import lyng.io.fs")
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can install with a custom policy too (see Access policy below).
|
You can install with a custom policy too (see Access policy below).
|
||||||
@ -185,7 +189,7 @@ val denyWrites = object : FsAccessPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createFs(denyWrites, scope)
|
createFs(denyWrites, scope)
|
||||||
scope.eval("import lyng.io.fs")
|
session.eval("import lyng.io.fs")
|
||||||
```
|
```
|
||||||
|
|
||||||
Composite operations like `copy` and `move` are checked as a set of primitives (e.g., `OpenRead(src)` + `Delete(dst)` if overwriting + `CreateFile(dst)` + `OpenWrite(dst)`).
|
Composite operations like `copy` and `move` are checked as a set of primitives (e.g., `OpenRead(src)` + `Delete(dst)` if overwriting + `CreateFile(dst)` + `OpenWrite(dst)`).
|
||||||
|
|||||||
@ -20,24 +20,26 @@ For external projects, ensure you have the appropriate Maven repository configur
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Install the module into a Lyng Scope
|
#### Install the module into a Lyng session
|
||||||
|
|
||||||
The process module is not installed automatically. You must explicitly register it in the scope’s `ImportManager` using `createProcessModule`. You can customize access control via `ProcessAccessPolicy`.
|
The process module is not installed automatically. The preferred host runtime is `EvalSession`: create the session, get its underlying scope, install the module there, and execute scripts through the session. You can customize access control via `ProcessAccessPolicy`.
|
||||||
|
|
||||||
Kotlin (host) bootstrap example:
|
Kotlin (host) bootstrap example:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.EvalSession
|
||||||
import net.sergeych.lyng.io.process.createProcessModule
|
import net.sergeych.lyng.io.process.createProcessModule
|
||||||
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||||
|
|
||||||
// ... inside a suspend function or runBlocking
|
suspend fun bootstrapProcess() {
|
||||||
val scope: Scope = Script.newScope()
|
val session = EvalSession()
|
||||||
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
val scope: Scope = session.getScope()
|
||||||
|
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||||
|
|
||||||
// In scripts (or via scope.eval), import the module:
|
// In scripts (or via session.eval), import the module:
|
||||||
scope.eval("import lyng.io.process")
|
session.eval("import lyng.io.process")
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -37,7 +37,7 @@ dependencies {
|
|||||||
To use `lyngio` modules in your scripts, you must install them into your Lyng scope and provide a security policy.
|
To use `lyngio` modules in your scripts, you must install them into your Lyng scope and provide a security policy.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.EvalSession
|
||||||
import net.sergeych.lyng.io.fs.createFs
|
import net.sergeych.lyng.io.fs.createFs
|
||||||
import net.sergeych.lyng.io.process.createProcessModule
|
import net.sergeych.lyng.io.process.createProcessModule
|
||||||
import net.sergeych.lyng.io.console.createConsoleModule
|
import net.sergeych.lyng.io.console.createConsoleModule
|
||||||
@ -46,7 +46,8 @@ import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
|
|||||||
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
||||||
|
|
||||||
suspend fun runMyScript() {
|
suspend fun runMyScript() {
|
||||||
val scope = Script.newScope()
|
val session = EvalSession()
|
||||||
|
val scope = session.getScope()
|
||||||
|
|
||||||
// Install modules with policies
|
// Install modules with policies
|
||||||
createFs(PermitAllAccessPolicy, scope)
|
createFs(PermitAllAccessPolicy, scope)
|
||||||
@ -54,7 +55,7 @@ suspend fun runMyScript() {
|
|||||||
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
||||||
|
|
||||||
// Now scripts can import them
|
// Now scripts can import them
|
||||||
scope.eval("""
|
session.eval("""
|
||||||
import lyng.io.fs
|
import lyng.io.fs
|
||||||
import lyng.io.process
|
import lyng.io.process
|
||||||
import lyng.io.console
|
import lyng.io.console
|
||||||
|
|||||||
@ -114,10 +114,12 @@ When running end‑to‑end “book” workloads or heavier benches, you can ena
|
|||||||
Flags are mutable at runtime, e.g.:
|
Flags are mutable at runtime, e.g.:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
PerfFlags.ARG_BUILDER = false
|
runTest {
|
||||||
val r1 = (Scope().eval(script) as ObjInt).value
|
PerfFlags.ARG_BUILDER = false
|
||||||
PerfFlags.ARG_BUILDER = true
|
val r1 = (EvalSession(Scope()).eval(script) as ObjInt).value
|
||||||
val r2 = (Scope().eval(script) as ObjInt).value
|
PerfFlags.ARG_BUILDER = true
|
||||||
|
val r2 = (EvalSession(Scope()).eval(script) as ObjInt).value
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Reset flags at the end of a test to avoid impacting other tests.
|
Reset flags at the end of a test to avoid impacting other tests.
|
||||||
@ -619,4 +621,3 @@ Reproduce
|
|||||||
Notes
|
Notes
|
||||||
- Negative caches are installed only after a real miss throws (cache‑after‑miss), preserving error semantics and invalidation on `layoutVersion` changes.
|
- Negative caches are installed only after a real miss throws (cache‑after‑miss), preserving error semantics and invalidation on `layoutVersion` changes.
|
||||||
- IndexRef PIC augments the existing direct path and uses move‑to‑front promotion; it is keyed on `(classId, layoutVersion)` like other PICs.
|
- IndexRef PIC augments the existing direct path and uses move‑to‑front promotion; it is keyed on `(classId, layoutVersion)` like other PICs.
|
||||||
|
|
||||||
|
|||||||
129
lynglib/src/commonMain/kotlin/net/sergeych/lyng/EvalSession.kt
Normal file
129
lynglib/src/commonMain/kotlin/net/sergeych/lyng/EvalSession.kt
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Host-managed lifetime owner for coroutines started from Lyng scripts.
|
||||||
|
*
|
||||||
|
* The session reuses a single [Scope] across eval calls and tracks async work launched by
|
||||||
|
* `launch { ... }` and active flow producers created from these evals.
|
||||||
|
*/
|
||||||
|
class EvalSession(initialScope: Scope? = null) {
|
||||||
|
private val ownerJob = SupervisorJob()
|
||||||
|
private val evalMutex = Mutex()
|
||||||
|
private val scopeMutex = Mutex()
|
||||||
|
private val activeJobs = MutableStateFlow(0)
|
||||||
|
|
||||||
|
private var _scope: Scope? = initialScope
|
||||||
|
val scope: Scope? get() = _scope
|
||||||
|
|
||||||
|
val isActive: Boolean get() = ownerJob.isActive
|
||||||
|
val isCancelled: Boolean get() = ownerJob.isCancelled
|
||||||
|
|
||||||
|
suspend fun getScope(): Scope = ensureScope()
|
||||||
|
|
||||||
|
suspend fun eval(code: String): Obj = eval(code.toSource())
|
||||||
|
|
||||||
|
suspend fun eval(source: Source): Obj = evalMutex.withLock {
|
||||||
|
throwIfCancelled()
|
||||||
|
val scope = ensureScope()
|
||||||
|
withEvalSession(this@EvalSession) {
|
||||||
|
scope.eval(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel(cause: String? = null) {
|
||||||
|
ownerJob.cancel(ScriptSessionCancelled(cause ?: "EvalSession cancelled"))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun join() {
|
||||||
|
activeJobs.filter { it == 0 }.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun cancelAndJoin() {
|
||||||
|
cancel()
|
||||||
|
join()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal suspend fun <T> launchTrackedDeferred(block: suspend CoroutineScope.() -> T): Deferred<T> {
|
||||||
|
throwIfCancelled()
|
||||||
|
val deferred = CoroutineScope(currentTrackedCoroutineContext()).async(block = block)
|
||||||
|
track(deferred)
|
||||||
|
return deferred
|
||||||
|
}
|
||||||
|
|
||||||
|
internal suspend fun launchTrackedJob(block: suspend CoroutineScope.() -> Unit): Job {
|
||||||
|
throwIfCancelled()
|
||||||
|
val job = CoroutineScope(currentTrackedCoroutineContext()).launch(block = block)
|
||||||
|
track(job)
|
||||||
|
return job
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun currentTrackedCoroutineContext(): CoroutineContext {
|
||||||
|
val base = currentCoroutineContext()
|
||||||
|
return base.minusKey(Job) + ownerJob + EvalSessionElement(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun ensureScope(): Scope {
|
||||||
|
_scope?.let { return it }
|
||||||
|
return scopeMutex.withLock {
|
||||||
|
_scope ?: Script.newScope().also { _scope = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun track(job: Job) {
|
||||||
|
activeJobs.update { it + 1 }
|
||||||
|
job.invokeOnCompletion {
|
||||||
|
activeJobs.update { count -> if (count > 0) count - 1 else 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun throwIfCancelled() {
|
||||||
|
if (ownerJob.isCancelled) {
|
||||||
|
throw ScriptSessionCancelled("EvalSession cancelled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
suspend fun currentOrNull(): EvalSession? = currentCoroutineContext()[EvalSessionElement]?.session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class EvalSessionElement(val session: EvalSession) :
|
||||||
|
AbstractCoroutineContextElement(EvalSessionElement) {
|
||||||
|
companion object Key : CoroutineContext.Key<EvalSessionElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
internal suspend fun <T> withEvalSession(session: EvalSession, block: suspend () -> T): T {
|
||||||
|
return kotlinx.coroutines.withContext(EvalSessionElement(session)) {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScriptSessionCancelled(message: String) : CancellationException(message)
|
||||||
@ -636,9 +636,17 @@ class Script(
|
|||||||
addFn("launch") {
|
addFn("launch") {
|
||||||
val callable = requireOnlyArg<Obj>()
|
val callable = requireOnlyArg<Obj>()
|
||||||
val captured = this
|
val captured = this
|
||||||
ObjDeferred(globalDefer {
|
val session = EvalSession.currentOrNull()
|
||||||
captured.call(callable)
|
val deferred = if (session != null) {
|
||||||
})
|
session.launchTrackedDeferred {
|
||||||
|
captured.call(callable)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
globalDefer {
|
||||||
|
captured.call(callable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ObjDeferred(deferred)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFn("yield") {
|
addFn("yield") {
|
||||||
@ -649,7 +657,7 @@ class Script(
|
|||||||
addFn("flow", callSignature = CallSignature(tailBlockReceiverType = "FlowBuilder")) {
|
addFn("flow", callSignature = CallSignature(tailBlockReceiverType = "FlowBuilder")) {
|
||||||
// important is: current context contains closure often used in call;
|
// important is: current context contains closure often used in call;
|
||||||
// we'll need it for the producer
|
// we'll need it for the producer
|
||||||
ObjFlow(requireOnlyArg<Obj>(), requireScope())
|
ObjFlow(requireOnlyArg<Obj>(), requireScope(), EvalSession.currentOrNull())
|
||||||
}
|
}
|
||||||
|
|
||||||
val pi = ObjReal(PI)
|
val pi = ObjReal(PI)
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.ChannelResult
|
import kotlinx.coroutines.channels.ChannelResult
|
||||||
@ -24,6 +25,7 @@ import kotlinx.coroutines.channels.ReceiveChannel
|
|||||||
import kotlinx.coroutines.channels.SendChannel
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import net.sergeych.lyng.EvalSession
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.ScriptFlowIsNoMoreCollected
|
import net.sergeych.lyng.ScriptFlowIsNoMoreCollected
|
||||||
import net.sergeych.lyng.miniast.ParamDoc
|
import net.sergeych.lyng.miniast.ParamDoc
|
||||||
@ -72,25 +74,34 @@ class ObjFlowBuilder(val output: SendChannel<Obj>) : Obj() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createLyngFlowInput(scope: Scope, producer: Obj): ReceiveChannel<Obj> {
|
private suspend fun createLyngFlowInput(scope: Scope, producer: Obj, ownerSession: EvalSession?): ReceiveChannel<Obj> {
|
||||||
val channel = Channel<Obj>(Channel.RENDEZVOUS)
|
val channel = Channel<Obj>(Channel.RENDEZVOUS)
|
||||||
val builder = ObjFlowBuilder(channel)
|
val builder = ObjFlowBuilder(channel)
|
||||||
val builderScope = scope.createChildScope(newThisObj = builder)
|
val builderScope = scope.createChildScope(newThisObj = builder)
|
||||||
globalLaunch {
|
val runProducer: suspend CoroutineScope.() -> Unit = {
|
||||||
|
var failure: Throwable? = null
|
||||||
try {
|
try {
|
||||||
producer.callOn(builderScope)
|
producer.callOn(builderScope)
|
||||||
} catch (x: ScriptFlowIsNoMoreCollected) {
|
} catch (x: ScriptFlowIsNoMoreCollected) {
|
||||||
// premature flow closing, OK
|
// premature flow closing, OK
|
||||||
} catch (x: Throwable) {
|
} catch (x: Throwable) {
|
||||||
channel.close(x)
|
failure = x
|
||||||
return@globalLaunch
|
|
||||||
}
|
}
|
||||||
channel.close()
|
if (failure != null) {
|
||||||
|
channel.close(failure)
|
||||||
|
} else {
|
||||||
|
channel.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ownerSession != null) {
|
||||||
|
ownerSession.launchTrackedJob(runProducer)
|
||||||
|
} else {
|
||||||
|
globalLaunch(block = runProducer)
|
||||||
}
|
}
|
||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
|
|
||||||
class ObjFlow(val producer: Obj, val scope: Scope) : Obj() {
|
class ObjFlow(val producer: Obj, val scope: Scope, val ownerSession: EvalSession? = null) : Obj() {
|
||||||
|
|
||||||
override val objClass get() = type
|
override val objClass get() = type
|
||||||
|
|
||||||
@ -109,14 +120,14 @@ class ObjFlow(val producer: Obj, val scope: Scope) : Obj() {
|
|||||||
val objFlow = thisAs<ObjFlow>()
|
val objFlow = thisAs<ObjFlow>()
|
||||||
ObjFlowIterator(ObjExternCallable.fromBridge {
|
ObjFlowIterator(ObjExternCallable.fromBridge {
|
||||||
call(objFlow.producer)
|
call(objFlow.producer)
|
||||||
})
|
}, objFlow.ownerSession)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ObjFlowIterator(val producer: Obj) : Obj() {
|
class ObjFlowIterator(val producer: Obj, val ownerSession: EvalSession? = null) : Obj() {
|
||||||
|
|
||||||
override val objClass: ObjClass get() = type
|
override val objClass: ObjClass get() = type
|
||||||
|
|
||||||
@ -134,7 +145,7 @@ class ObjFlowIterator(val producer: Obj) : Obj() {
|
|||||||
suspend fun hasNext(scope: Scope): ObjBool {
|
suspend fun hasNext(scope: Scope): ObjBool {
|
||||||
checkNotCancelled(scope)
|
checkNotCancelled(scope)
|
||||||
// cold start:
|
// cold start:
|
||||||
if (channel == null) channel = createLyngFlowInput(scope, producer)
|
if (channel == null) channel = createLyngFlowInput(scope, producer, ownerSession)
|
||||||
if (nextItem == null) nextItem = channel!!.receiveCatching()
|
if (nextItem == null) nextItem = channel!!.receiveCatching()
|
||||||
nextItem?.exceptionOrNull()?.let { throw it }
|
nextItem?.exceptionOrNull()?.let { throw it }
|
||||||
return ObjBool(nextItem!!.isSuccess)
|
return ObjBool(nextItem!!.isSuccess)
|
||||||
|
|||||||
160
lynglib/src/commonTest/kotlin/EvalSessionTest.kt
Normal file
160
lynglib/src/commonTest/kotlin/EvalSessionTest.kt
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.EvalSession
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.obj.ObjBool
|
||||||
|
import net.sergeych.lyng.obj.ObjFlow
|
||||||
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
|
import net.sergeych.lyng.obj.ObjList
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
class EvalSessionTest {
|
||||||
|
@Test
|
||||||
|
fun sessionCreatesAndReusesScope() = runTest {
|
||||||
|
val session = EvalSession()
|
||||||
|
|
||||||
|
assertEquals(null, session.scope)
|
||||||
|
assertEquals(10L, (session.eval("var x = 10; x") as ObjInt).value)
|
||||||
|
assertNotNull(session.scope)
|
||||||
|
assertEquals(session.scope, session.getScope())
|
||||||
|
assertEquals(15L, (session.eval("x += 5; x") as ObjInt).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sessionCancelStopsLaunchedCoroutines() = runTest {
|
||||||
|
val scope = Scope()
|
||||||
|
val session = EvalSession(scope)
|
||||||
|
|
||||||
|
session.eval(
|
||||||
|
"""
|
||||||
|
var touched = false
|
||||||
|
launch {
|
||||||
|
delay(100)
|
||||||
|
touched = true
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
.trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
session.cancel()
|
||||||
|
session.join()
|
||||||
|
advanceTimeBy(150)
|
||||||
|
|
||||||
|
assertFalse((scope.eval("touched") as ObjBool).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun joinObservesWorkStartedByLaterEval() = runTest {
|
||||||
|
val session = EvalSession(Scope())
|
||||||
|
|
||||||
|
session.eval("launch { delay(100) }")
|
||||||
|
|
||||||
|
var joined = false
|
||||||
|
val waiter = launch {
|
||||||
|
session.join()
|
||||||
|
joined = true
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceTimeBy(50)
|
||||||
|
assertFalse(joined)
|
||||||
|
|
||||||
|
session.eval("launch { delay(100) }")
|
||||||
|
|
||||||
|
advanceTimeBy(60)
|
||||||
|
assertFalse(joined)
|
||||||
|
|
||||||
|
advanceTimeBy(50)
|
||||||
|
waiter.join()
|
||||||
|
assertTrue(joined)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun concurrentEvalCallsAreSerialized() = runTest {
|
||||||
|
val session = EvalSession(Scope())
|
||||||
|
|
||||||
|
session.eval("var counter = 0")
|
||||||
|
|
||||||
|
val first = async {
|
||||||
|
session.eval(
|
||||||
|
"""
|
||||||
|
delay(100)
|
||||||
|
counter += 1
|
||||||
|
counter
|
||||||
|
"""
|
||||||
|
.trimIndent()
|
||||||
|
) as ObjInt
|
||||||
|
}
|
||||||
|
val second = async {
|
||||||
|
session.eval(
|
||||||
|
"""
|
||||||
|
counter += 10
|
||||||
|
counter
|
||||||
|
"""
|
||||||
|
.trimIndent()
|
||||||
|
) as ObjInt
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceTimeBy(150)
|
||||||
|
|
||||||
|
assertEquals(1L, first.await().value)
|
||||||
|
assertEquals(11L, second.await().value)
|
||||||
|
assertEquals(11L, (session.eval("counter") as ObjInt).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun joinWaitsForActiveFlowProducer() = runTest {
|
||||||
|
val scope = Scope()
|
||||||
|
val session = EvalSession(scope)
|
||||||
|
val flow = session.eval(
|
||||||
|
"""
|
||||||
|
flow {
|
||||||
|
delay(100)
|
||||||
|
emit(1)
|
||||||
|
delay(100)
|
||||||
|
emit(2)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
.trimIndent()
|
||||||
|
) as ObjFlow
|
||||||
|
|
||||||
|
var joined = false
|
||||||
|
var collected: ObjList? = null
|
||||||
|
|
||||||
|
val collector = launch {
|
||||||
|
collected = flow.callMethod(scope, "toList")
|
||||||
|
}
|
||||||
|
val waiter = launch {
|
||||||
|
session.join()
|
||||||
|
joined = true
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceTimeBy(150)
|
||||||
|
assertFalse(joined)
|
||||||
|
|
||||||
|
advanceTimeBy(100)
|
||||||
|
collector.join()
|
||||||
|
waiter.join()
|
||||||
|
|
||||||
|
assertTrue(joined)
|
||||||
|
assertEquals(listOf(1L, 2L), collected!!.list.map { (it as ObjInt).value })
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user