diff --git a/docs/embedding.md b/docs/embedding.md index 1e1125f..3c16788 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -4,7 +4,7 @@ Lyng is a tiny, embeddable, Kotlin‑first scripting language. This page shows, - add Lyng to your build - create a runtime and execute scripts -- define functions and variables from Kotlin +- declare extern globals in Lyng and bind them from Kotlin - read variable values back in Kotlin - call Lyng functions from Kotlin - create your own packages and import them in Lyng @@ -65,30 +65,74 @@ val run2 = script.execute(scope) `Scope.eval("...")` is a shortcut that compiles and executes on the given scope. -### 3) Define variables from Kotlin +### 3) Preferred: bind extern globals from Kotlin -To expose data to Lyng, add constants (read‑only) or mutable variables to the scope. All values in Lyng are `Obj` instances; the core types live in `net.sergeych.lyng.obj`. +For module-level APIs, the default workflow is: + +1. declare globals in Lyng using `extern fun` / `extern val` / `extern var`; +2. bind Kotlin implementation via `ModuleScope.globalBinder()`. ```kotlin -// Read‑only constant -scope.addConst("pi", ObjReal(3.14159)) +import net.sergeych.lyng.bridge.* +import net.sergeych.lyng.obj.ObjInt +import net.sergeych.lyng.obj.ObjString -// Mutable variable: create or update -scope.addOrUpdateItem("counter", ObjInt(0)) +val im = Script.defaultImportManager.copy() +im.addPackage("my.api") { module -> + module.eval(""" + extern fun globalFun(v: Int): Int + extern var globalProp: String + extern val globalVersion: String + """.trimIndent()) -// Use it from Lyng -scope.eval("counter = counter + 1") + val binder = module.globalBinder() + + binder.bindGlobalFun1("globalFun") { v -> + ObjInt.of((v + 1).toLong()) + } + + var prop = "initial" + binder.bindGlobalVar( + name = "globalProp", + get = { prop }, + set = { prop = it } + ) + + binder.bindGlobalVar( + name = "globalVersion", + get = { "1.0.0" } // readonly: setter omitted + ) +} ``` -Tip: Lyng values can be converted back to Kotlin with `toKotlin(scope)`: +Usage from Lyng: + +```lyng +import my.api + +assertEquals(42, globalFun(41)) +assertEquals("initial", globalProp) +globalProp = "changed" +assertEquals("changed", globalProp) +assertEquals("1.0.0", globalVersion) +``` + +For custom argument handling and full runtime access: ```kotlin -val current = (scope.eval("counter")).toKotlin(scope) // Any? (e.g., Int/Double/String/List) +binder.bindGlobalFun("sum3") { + requireExactCount(3) + ObjInt.of((int(0) + int(1) + int(2)).toLong()) +} + +binder.bindGlobalFunRaw("echoRaw") { _, args -> + args.firstAndOnly() +} ``` -### 4) Add Kotlin‑backed functions +### 4) Low-level: direct functions/variables from Kotlin -Use `Scope.addFn`/`addVoidFn` to register functions implemented in Kotlin. Inside the lambda, use `this.args` to access arguments and return an `Obj`. +Use this when you intentionally want raw `Scope` APIs. For most module APIs, prefer section 3. ```kotlin // A function returning value @@ -280,6 +324,22 @@ Notes: - Members must be marked `extern` so the compiler emits ABI slots for Kotlin bindings. - You can also bind by name/module via `LyngObjectBridge.bind(...)`. +Minimal `extern fun` example: + +```kotlin +val moduleScope = importManager.createModuleScope(Pos.builtIn, "bridge.ping") + +moduleScope.eval(""" + extern object HostObject { + extern fun ping(): Int + } +""".trimIndent()) + +moduleScope.bindObject("HostObject") { + addFun("ping") { _, _, _ -> ObjInt.of(7) } +} +``` + ### 6.6) Preferred: Kotlin reflection bridge for call‑by‑name For Kotlin code that needs dynamic access to Lyng variables, functions, or members, use the bridge resolver. @@ -368,6 +428,9 @@ Key concepts: Register a Kotlin‑built package: ```kotlin +import net.sergeych.lyng.bridge.* +import net.sergeych.lyng.obj.ObjInt + val scope = Script.newScope() // Access the import manager behind this scope @@ -375,11 +438,19 @@ val im: ImportManager = scope.importManager // Register a package "my.tools" im.addPackage("my.tools") { module: ModuleScope -> - // Expose symbols inside the module scope - module.addConst("version", ObjString("1.0")) - module.addFn("triple") { - val x = args.firstAndOnly() as ObjInt - ObjInt(x.value * 3) + module.eval( + """ + extern val version: String + extern fun triple(x: Int): Int + """.trimIndent() + ) + val binder = module.globalBinder() + binder.bindGlobalVar( + name = "version", + get = { "1.0" } + ) + binder.bindGlobalFun1("triple") { x -> + ObjInt.of((x * 3).toLong()) } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/GlobalBridge.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/GlobalBridge.kt new file mode 100644 index 0000000..7d62c65 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/GlobalBridge.kt @@ -0,0 +1,264 @@ +/* + * 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.bridge + +import net.sergeych.lyng.* +import net.sergeych.lyng.obj.Obj +import net.sergeych.lyng.obj.ObjBool +import net.sergeych.lyng.obj.ObjExternCallable +import net.sergeych.lyng.obj.ObjInt +import net.sergeych.lyng.obj.ObjNull +import net.sergeych.lyng.obj.ObjProperty +import net.sergeych.lyng.obj.ObjReal +import net.sergeych.lyng.obj.ObjRecord +import net.sergeych.lyng.obj.ObjString +import net.sergeych.lyng.obj.ObjVoid +import net.sergeych.lyng.obj.toBool +import net.sergeych.lyng.obj.toDouble +import net.sergeych.lyng.obj.toInt +import net.sergeych.lyng.obj.toLong +import net.sergeych.lyng.obj.toObj +import net.sergeych.lyng.requiredArg + +/** + * Global/module-level binding API for Lyng-first extern declarations. + * + * Typical flow: + * 1) declare `extern fun` / `extern val` / `extern var` in Lyng module; + * 2) bind Kotlin implementation using this API. + */ +interface LyngGlobalBinder { + fun bindGlobalFunRaw( + name: String, + fn: suspend (scope: ScopeFacade, args: Arguments) -> Obj + ) + + fun bindGlobalFun( + name: String, + fn: suspend GlobalArgReader.() -> Obj + ) + + fun bindGlobalVarRaw( + name: String, + get: suspend (scope: ScopeFacade) -> Obj, + set: (suspend (scope: ScopeFacade, value: Obj) -> Unit)? = null + ) +} + +/** + * Reader helper for Kotlin-typed argument access. + */ +interface GlobalArgReader { + val scope: ScopeFacade + val args: Arguments + val size: Int + + fun requireExactCount(count: Int) + fun obj(index: Int): Obj + fun objOrNull(index: Int): Obj? + fun int(index: Int): Int + fun long(index: Int): Long + fun double(index: Int): Double + fun bool(index: Int): Boolean + fun string(index: Int): String +} + +private class ModuleGlobalBinder( + private val module: ModuleScope +) : LyngGlobalBinder { + + override fun bindGlobalFunRaw( + name: String, + fn: suspend (scope: ScopeFacade, args: Arguments) -> Obj + ) { + val existing = module[name] + val callable = ObjExternCallable.fromBridge { + fn(this, args) + } + module.addItem( + name = name, + isMutable = false, + value = callable, + visibility = existing?.visibility ?: Visibility.Public, + writeVisibility = existing?.writeVisibility, + recordType = ObjRecord.Type.Fun, + callSignature = existing?.callSignature, + typeDecl = existing?.typeDecl + ) + } + + override fun bindGlobalFun( + name: String, + fn: suspend GlobalArgReader.() -> Obj + ) { + bindGlobalFunRaw(name) { scope, args -> + val reader = GlobalArgReaderImpl(scope, args) + fn(reader) + } + } + + override fun bindGlobalVarRaw( + name: String, + get: suspend (scope: ScopeFacade) -> Obj, + set: (suspend (scope: ScopeFacade, value: Obj) -> Unit)? + ) { + val existing = module[name] + if (existing != null) { + if (existing.isMutable && set == null) { + throw net.sergeych.lyng.ScriptError(Pos.builtIn, "extern var $name requires a setter") + } + if (!existing.isMutable && set != null) { + throw net.sergeych.lyng.ScriptError(Pos.builtIn, "extern val $name does not allow a setter") + } + } + val mutable = existing?.isMutable ?: (set != null) + val getter = ObjExternCallable.fromBridge { + get(this) + } + val setter = set?.let { setterImpl -> + ObjExternCallable.fromBridge { + setterImpl(this, requiredArg(0)) + ObjVoid + } + } + module.addItem( + name = name, + isMutable = mutable, + value = ObjProperty(name, getter, setter), + visibility = existing?.visibility ?: Visibility.Public, + writeVisibility = existing?.writeVisibility, + recordType = ObjRecord.Type.Property, + callSignature = existing?.callSignature, + typeDecl = existing?.typeDecl + ) + } +} + +private class GlobalArgReaderImpl( + override val scope: ScopeFacade, + override val args: Arguments +) : GlobalArgReader { + override val size: Int + get() = args.list.size + + override fun requireExactCount(count: Int) { + if (size != count) scope.raiseIllegalArgument("Expected exactly $count arguments, got $size") + } + + override fun obj(index: Int): Obj = + objOrNull(index) ?: scope.raiseIllegalArgument("Missing required argument at index $index") + + override fun objOrNull(index: Int): Obj? = + args.list.getOrNull(index) + + override fun int(index: Int): Int = long(index).toInt() + + override fun long(index: Int): Long = obj(index).toLong() + + override fun double(index: Int): Double = obj(index).toDouble() + + override fun bool(index: Int): Boolean = obj(index).toBool() + + override fun string(index: Int): String { + val value = obj(index) + return (value as? ObjString)?.value + ?: scope.raiseClassCastError("Expected String at index $index, got ${value.objClass.className}") + } +} + +fun ModuleScope.globalBinder(): LyngGlobalBinder = ModuleGlobalBinder(this) + +inline fun GlobalArgReader.required(index: Int): T = + coerceArg(scope, obj(index), index) + +inline fun GlobalArgReader.optional(index: Int, default: T): T { + val value = objOrNull(index) ?: return default + return coerceArg(scope, value, index) +} + +inline fun LyngGlobalBinder.bindGlobalFun1( + name: String, + noinline fn: suspend (A1) -> Obj +) { + bindGlobalFun(name) { + requireExactCount(1) + fn(required(0)) + } +} + +inline fun LyngGlobalBinder.bindGlobalFun2( + name: String, + noinline fn: suspend (A1, A2) -> Obj +) { + bindGlobalFun(name) { + requireExactCount(2) + fn(required(0), required(1)) + } +} + +inline fun LyngGlobalBinder.bindGlobalFun3( + name: String, + noinline fn: suspend (A1, A2, A3) -> Obj +) { + bindGlobalFun(name) { + requireExactCount(3) + fn(required(0), required(1), required(2)) + } +} + +inline fun LyngGlobalBinder.bindGlobalVar( + name: String, + noinline get: suspend () -> T, + noinline set: (suspend (T) -> Unit)? = null +) { + bindGlobalVarRaw( + name = name, + get = { get().toObj() }, + set = set?.let { setter -> + { scope, value -> + setter(coerceArg(scope = scope, value = value, index = 0)) + } + } + ) +} + +@PublishedApi +internal inline fun coerceArg(scope: ScopeFacade, value: Obj, index: Int): T { + if (value === ObjNull && null is T) return null as T + (value as? T)?.let { return it } + @Suppress("UNCHECKED_CAST") + return when (T::class) { + Int::class -> value.toInt() as T + Long::class -> value.toLong() as T + Double::class -> value.toDouble() as T + Float::class -> value.toDouble().toFloat() as T + Boolean::class -> value.toBool() as T + String::class -> (value as? ObjString)?.value as? T + ?: scope.raiseClassCastError("Expected String at index $index, got ${value.objClass.className}") + Obj::class -> value as T + ObjInt::class -> (value as? ObjInt) as? T + ?: scope.raiseClassCastError("Expected ObjInt at index $index, got ${value.objClass.className}") + ObjString::class -> (value as? ObjString) as? T + ?: scope.raiseClassCastError("Expected ObjString at index $index, got ${value.objClass.className}") + ObjReal::class -> (value as? ObjReal) as? T + ?: scope.raiseClassCastError("Expected ObjReal at index $index, got ${value.objClass.className}") + ObjBool::class -> (value as? ObjBool) as? T + ?: scope.raiseClassCastError("Expected ObjBool at index $index, got ${value.objClass.className}") + else -> scope.raiseClassCastError("Unsupported typed argument binding for ${T::class.simpleName}") + } +} diff --git a/lynglib/src/commonTest/kotlin/GlobalBindingTest.kt b/lynglib/src/commonTest/kotlin/GlobalBindingTest.kt new file mode 100644 index 0000000..cd671b3 --- /dev/null +++ b/lynglib/src/commonTest/kotlin/GlobalBindingTest.kt @@ -0,0 +1,139 @@ +/* + * 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.test.runTest +import net.sergeych.lyng.bridge.bindGlobalFun1 +import net.sergeych.lyng.bridge.bindGlobalFun3 +import net.sergeych.lyng.bridge.bindGlobalVar +import net.sergeych.lyng.bridge.globalBinder +import net.sergeych.lyng.obj.ObjInt +import net.sergeych.lyng.obj.ObjString +import kotlin.test.Test +import kotlin.test.assertTrue + +class GlobalBindingTest { + @Test + fun testPackageGlobalFunAndVarBinding() = runTest { + val im = Script.defaultImportManager.copy() + var prop = "initial" + im.addPackage("bridge.globals") { module -> + module.eval( + """ + extern fun globalFun(v: Int): Int + extern fun join3(a: String, b: String, c: String): String + extern var globalProp: String + extern val answer: Int + """.trimIndent() + ) + val binder = module.globalBinder() + binder.bindGlobalFun1("globalFun") { v -> + ObjInt.of((v + 10).toLong()) + } + binder.bindGlobalFun3("join3") { a, b, c -> + ObjString(a + b + c) + } + binder.bindGlobalVar( + name = "globalProp", + get = { prop }, + set = { prop = it } + ) + binder.bindGlobalVar( + name = "answer", + get = { 42 } + ) + } + + val scope = im.newStdScope() + scope.eval( + """ + import bridge.globals + assertEquals(15, globalFun(5)) + assertEquals("abc", join3("a", "b", "c")) + assertEquals("initial", globalProp) + globalProp = "changed" + assertEquals("changed", globalProp) + assertEquals(42, answer) + """.trimIndent() + ) + } + + @Test + fun testPackageGlobalRawAndArgReaderBinding() = runTest { + val im = Script.defaultImportManager.copy() + im.addPackage("bridge.raw") { module -> + module.eval( + """ + extern fun sum3(a: Int, b: Int, c: Int): Int + extern fun echoRaw(x: Int): Int + """.trimIndent() + ) + val binder = module.globalBinder() + binder.bindGlobalFun("sum3") { + requireExactCount(3) + ObjInt.of((int(0) + int(1) + int(2)).toLong()) + } + binder.bindGlobalFunRaw("echoRaw") { _, args -> + args.firstAndOnly() + } + } + val scope = im.newStdScope() + scope.eval( + """ + import bridge.raw + assertEquals(6, sum3(1, 2, 3)) + assertEquals(77, echoRaw(77)) + """.trimIndent() + ) + } + + @Test + fun testGlobalVarExternCompatibilityChecks() = runTest { + val im = Script.defaultImportManager.copy() + im.addPackage("bridge.compat") { module -> + module.eval( + """ + extern var needsSetter: String + extern val noSetterAllowed: String + """.trimIndent() + ) + val binder = module.globalBinder() + val missingSetter = try { + binder.bindGlobalVar( + name = "needsSetter", + get = { "x" } + ) + false + } catch (_: ScriptError) { + true + } + val readonlySetter = try { + binder.bindGlobalVar( + name = "noSetterAllowed", + get = { "x" }, + set = { _ -> } + ) + false + } catch (_: ScriptError) { + true + } + assertTrue(missingSetter) + assertTrue(readonlySetter) + } + } +}