Added EvalSession way of controlling all coroutines started from a script
This commit is contained in:
1 parent
446c8d9a6e
commit
fc01016a74
13 files changed
+482
-94
No files matched your search
+2
-2
@@ -80,11 +80,11 @@ If you already have an ionspin `BigDecimal` on the host side, the simplest suppo
|
||||
|
||||
```kotlin
|
||||
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.newDecimal
|
||||
|
||||
val scope = Script.newScope()
|
||||
val scope = EvalSession().getScope()
|
||||
val decimal = scope.asFacade().newDecimal(BigDecimal.parseStringWithMode("12.34"))
|
||||
```
|
||||
|
||||
|
||||
+90
-27
@@ -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).
|
||||
|
||||
### 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
|
||||
fun main() = kotlinx.coroutines.runBlocking {
|
||||
val scope = Script.newScope() // suspends on first init
|
||||
|
||||
// Evaluate a one‑liner
|
||||
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
|
||||
val script = Compiler.compile("""
|
||||
@@ -63,7 +102,8 @@ val run1 = 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
|
||||
|
||||
@@ -85,6 +125,8 @@ import net.sergeych.lyng.bridge.*
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
val im = Script.defaultImportManager.copy()
|
||||
im.addPackage("my.api") { module ->
|
||||
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.
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
|
||||
// A function returning value
|
||||
scope.addFn<ObjInt>("inc") {
|
||||
val x = args.firstAndOnly() as ObjInt
|
||||
@@ -167,7 +212,7 @@ scope.addVoidFn("log") {
|
||||
// }
|
||||
|
||||
// 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") { ... }`.
|
||||
@@ -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.
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
val myClass = ObjClass("MyClass")
|
||||
|
||||
// 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`.
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
val myClass = ObjClass("MyClass")
|
||||
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.
|
||||
|
||||
```kotlin
|
||||
val scope = Script.newScope()
|
||||
scope.eval("""
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
session.eval("""
|
||||
val x = 40
|
||||
fun add(a, b) = a + b
|
||||
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)))
|
||||
|
||||
// Member access
|
||||
val box = scope.eval("Box()")
|
||||
val box = session.eval("Box()")
|
||||
val valueHandle = resolver.resolveMemberVar(box, "value")
|
||||
valueHandle.set(scope, ObjInt(10))
|
||||
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.
|
||||
|
||||
```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:
|
||||
scope.addOrUpdateItem("name", ObjString("Lyng"))
|
||||
scope.eval("name = name + ' rocks!'")
|
||||
val kotlinName = scope.eval("name").toKotlin(scope) // -> "Lyng rocks!"
|
||||
session.eval("name = name + ' 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.
|
||||
@@ -492,16 +544,20 @@ There are two convenient patterns.
|
||||
|
||||
```kotlin
|
||||
// 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:
|
||||
|
||||
```kotlin
|
||||
// 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
|
||||
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.obj.ObjInt
|
||||
|
||||
val scope = Script.newScope()
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
|
||||
// Access the import manager behind this scope
|
||||
val im: ImportManager = scope.importManager
|
||||
@@ -563,12 +620,12 @@ im.addPackage("my.tools") { module: ModuleScope ->
|
||||
}
|
||||
|
||||
// Use it from Lyng
|
||||
scope.eval("""
|
||||
session.eval("""
|
||||
import my.tools.*
|
||||
val v = triple(14)
|
||||
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:
|
||||
@@ -582,24 +639,27 @@ val pkgText = """
|
||||
|
||||
scope.importManager.addTextPackages(pkgText)
|
||||
|
||||
scope.eval("""
|
||||
session.eval("""
|
||||
import math.extra.*
|
||||
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)`.
|
||||
|
||||
### 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.
|
||||
- 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
|
||||
// Fresh module based on the default manager, without the standard prelude
|
||||
val isolated = net.sergeych.lyng.Scope.new()
|
||||
// Preferred per-request runtime:
|
||||
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
|
||||
@@ -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.
|
||||
|
||||
```kotlin
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
|
||||
try {
|
||||
scope.eval("throw MyUserException(404, \"Not Found\")")
|
||||
session.eval("throw MyUserException(404, \"Not Found\")")
|
||||
} catch (e: ExecutionError) {
|
||||
// 1. Serialize the Lyng exception object
|
||||
val encoded: UByteArray = lynonEncodeAny(scope, e.errorObject)
|
||||
|
||||
@@ -122,7 +122,8 @@ data class TestJson2(
|
||||
|
||||
@Test
|
||||
fun deserializeMapWithJsonTest() = runTest {
|
||||
val x = eval("""
|
||||
val session = EvalSession()
|
||||
val x = session.eval("""
|
||||
import lyng.serialization
|
||||
{ value: 1, inner: { "foo": 1, "bar": 2 }}
|
||||
""".trimIndent()).decodeSerializable<TestJson2>()
|
||||
@@ -143,7 +144,8 @@ data class TestJson3(
|
||||
)
|
||||
@Test
|
||||
fun deserializeAnyMapWithJsonTest() = runTest {
|
||||
val x = eval("""
|
||||
val session = EvalSession()
|
||||
val x = session.eval("""
|
||||
import lyng.serialization
|
||||
{ value: 12, inner: { "foo": 1, "bar": "two" }}
|
||||
""".trimIndent()).decodeSerializable<TestJson3>()
|
||||
@@ -175,4 +177,3 @@ on [Instant](time.md), see `Instant.truncateTo...` functions.
|
||||
|
||||
(3)
|
||||
: Map keys must be strings, map values may be any objects serializable to Json.
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
#### Install in host
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.io.console.createConsoleModule
|
||||
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
|
||||
|
||||
suspend fun initScope() {
|
||||
val scope = Script.newScope()
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
||||
}
|
||||
```
|
||||
|
||||
+12
-8
@@ -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
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.io.fs.createFs
|
||||
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
|
||||
|
||||
val scope: Scope = Scope.new()
|
||||
val installed: Boolean = createFs(PermitAllAccessPolicy, scope)
|
||||
// installed == true on first registration in this ImportManager, false on repeats
|
||||
suspend fun bootstrapFs() {
|
||||
val session = EvalSession()
|
||||
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:
|
||||
scope.eval("import lyng.io.fs")
|
||||
// In scripts (or via session.eval), import the module to use its symbols:
|
||||
session.eval("import lyng.io.fs")
|
||||
}
|
||||
```
|
||||
|
||||
You can install with a custom policy too (see Access policy below).
|
||||
@@ -185,7 +189,7 @@ val denyWrites = object : FsAccessPolicy {
|
||||
}
|
||||
|
||||
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)`).
|
||||
|
||||
+10
-8
@@ -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
|
||||
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.lyngio.process.security.PermitAllProcessAccessPolicy
|
||||
|
||||
// ... inside a suspend function or runBlocking
|
||||
val scope: Scope = Script.newScope()
|
||||
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||
suspend fun bootstrapProcess() {
|
||||
val session = EvalSession()
|
||||
val scope: Scope = session.getScope()
|
||||
createProcessModule(PermitAllProcessAccessPolicy, scope)
|
||||
|
||||
// In scripts (or via scope.eval), import the module:
|
||||
scope.eval("import lyng.io.process")
|
||||
// In scripts (or via session.eval), import the module:
|
||||
session.eval("import lyng.io.process")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+4
-3
@@ -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.
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.EvalSession
|
||||
import net.sergeych.lyng.io.fs.createFs
|
||||
import net.sergeych.lyng.io.process.createProcessModule
|
||||
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
|
||||
|
||||
suspend fun runMyScript() {
|
||||
val scope = Script.newScope()
|
||||
val session = EvalSession()
|
||||
val scope = session.getScope()
|
||||
|
||||
// Install modules with policies
|
||||
createFs(PermitAllAccessPolicy, scope)
|
||||
@@ -54,7 +55,7 @@ suspend fun runMyScript() {
|
||||
createConsoleModule(PermitAllConsoleAccessPolicy, scope)
|
||||
|
||||
// Now scripts can import them
|
||||
scope.eval("""
|
||||
session.eval("""
|
||||
import lyng.io.fs
|
||||
import lyng.io.process
|
||||
import lyng.io.console
|
||||
|
||||
+6
-5
@@ -114,10 +114,12 @@ When running end‑to‑end “book” workloads or heavier benches, you can ena
|
||||
Flags are mutable at runtime, e.g.:
|
||||
|
||||
```kotlin
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
val r1 = (Scope().eval(script) as ObjInt).value
|
||||
PerfFlags.ARG_BUILDER = true
|
||||
val r2 = (Scope().eval(script) as ObjInt).value
|
||||
runTest {
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
val r1 = (EvalSession(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.
|
||||
@@ -619,4 +621,3 @@ Reproduce
|
||||
Notes
|
||||
- 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.
|
||||
|
||||
Reference in new issue
Block a user