From e7c1adb2c5a917a53c79482b07a0b9c30240f6bc Mon Sep 17 00:00:00 2001 From: sergeych Date: Thu, 19 Feb 2026 08:12:28 +0300 Subject: [PATCH] Kotlin bride: object bindings added --- .../net/sergeych/lyng/bridge/ClassBridge.kt | 286 ++++++++++++++++++ .../commonTest/kotlin/BridgeBindingTest.kt | 74 ++++- 2 files changed, 357 insertions(+), 3 deletions(-) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/ClassBridge.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/ClassBridge.kt index 8e85e11..15ae8ef 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/ClassBridge.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/ClassBridge.kt @@ -122,6 +122,51 @@ object LyngClassBridge { } } +/** + * Entry point for Kotlin bindings to declared Lyng objects (singleton instances). + * + * Works similarly to [LyngClassBridge], but targets an already created object instance. + */ +object LyngObjectBridge { + /** + * Resolve a Lyng object by [objectName] and bind Kotlin implementations. + * + * @param module module name used for resolution (required when [module] scope is not provided) + * @param importManager import manager used to resolve the module + */ + suspend fun bind( + objectName: String, + module: String? = null, + importManager: ImportManager = Script.defaultImportManager, + block: ClassBridgeBinder.() -> Unit + ): ObjInstance { + val obj = resolveObject(objectName, module, null, importManager) + return bind(obj, block) + } + + /** + * Resolve a Lyng object within an existing [moduleScope] and bind Kotlin implementations. + */ + suspend fun bind( + moduleScope: ModuleScope, + objectName: String, + block: ClassBridgeBinder.() -> Unit + ): ObjInstance { + val obj = resolveObject(objectName, null, moduleScope, Script.defaultImportManager) + return bind(obj, block) + } + + /** + * Bind Kotlin implementations directly to an already resolved object [instance]. + */ + suspend fun bind(instance: ObjInstance, block: ClassBridgeBinder.() -> Unit): ObjInstance { + val binder = ObjectBridgeBinderImpl(instance) + binder.block() + binder.commit() + return instance + } +} + /** * Sugar for [LyngClassBridge.bind] on a module scope. * @@ -132,6 +177,16 @@ suspend fun ModuleScope.bind( block: ClassBridgeBinder.() -> Unit ): ObjClass = LyngClassBridge.bind(this, className, block) +/** + * Sugar for [LyngObjectBridge.bind] on a module scope. + * + * Bound members must be declared as `extern` in Lyng. + */ +suspend fun ModuleScope.bindObject( + objectName: String, + block: ClassBridgeBinder.() -> Unit +): ObjInstance = LyngObjectBridge.bind(this, objectName, block) + /** Kotlin-side data slot attached to a Lyng instance. */ var ObjInstance.data: Any? get() = kotlinInstanceData @@ -327,6 +382,214 @@ private class ClassBridgeBinderImpl( } } +private class ObjectBridgeBinderImpl( + private val instance: ObjInstance +) : ClassBridgeBinder { + private val cls: ObjClass = instance.objClass + private val initHooks = mutableListOf Unit>() + + override var classData: Any? + get() = cls.kotlinClassData + set(value) { cls.kotlinClassData = value } + + override fun init(block: suspend BridgeInstanceContext.(ScopeFacade) -> Unit) { + initHooks.add { scope, inst -> + val ctx = BridgeInstanceContextImpl(inst) + ctx.block(scope) + } + } + + override fun initWithInstance(block: suspend (ScopeFacade, Obj) -> Unit) { + initHooks.add { scope, inst -> + block(scope, inst) + } + } + + override fun addFun(name: String, impl: suspend (ScopeFacade, Obj, Arguments) -> Obj) { + val target = findMember(name) + val callable = ObjExternCallable.fromBridge { + impl(this, thisObj, args) + } + val methodId = cls.ensureMethodIdForBridge(name, target.record) + val newRecord = target.record.copy( + value = callable, + type = ObjRecord.Type.Fun, + methodId = methodId + ) + replaceMember(target, newRecord) + updateInstanceMember(target, newRecord) + } + + override fun addVal(name: String, impl: suspend (ScopeFacade, Obj) -> Obj) { + val target = findMember(name) + if (target.record.isMutable) { + throw ScriptError(Pos.builtIn, "extern val $name is mutable in class ${cls.className}") + } + val getter = ObjExternCallable.fromBridge { + impl(this, thisObj) + } + val prop = ObjProperty(name, getter, null) + val isFieldLike = target.record.type == ObjRecord.Type.Field || + target.record.type == ObjRecord.Type.ConstructorField + val newRecord = if (isFieldLike) { + removeFieldInitializersFor(name) + target.record.copy( + value = prop, + type = target.record.type, + fieldId = target.record.fieldId, + methodId = target.record.methodId + ) + } else { + val methodId = cls.ensureMethodIdForBridge(name, target.record) + target.record.copy( + value = prop, + type = ObjRecord.Type.Property, + methodId = methodId, + fieldId = null + ) + } + replaceMember(target, newRecord) + updateInstanceMember(target, newRecord) + } + + override fun addVar( + name: String, + get: suspend (ScopeFacade, Obj) -> Obj, + set: suspend (ScopeFacade, Obj, Obj) -> Unit + ) { + val target = findMember(name) + if (!target.record.isMutable) { + throw ScriptError(Pos.builtIn, "extern var $name is readonly in class ${cls.className}") + } + val getter = ObjExternCallable.fromBridge { + get(this, thisObj) + } + val setter = ObjExternCallable.fromBridge { + val value = requiredArg(0) + set(this, thisObj, value) + ObjVoid + } + val prop = ObjProperty(name, getter, setter) + val isFieldLike = target.record.type == ObjRecord.Type.Field || + target.record.type == ObjRecord.Type.ConstructorField + val newRecord = if (isFieldLike) { + removeFieldInitializersFor(name) + target.record.copy( + value = prop, + type = target.record.type, + fieldId = target.record.fieldId, + methodId = target.record.methodId + ) + } else { + val methodId = cls.ensureMethodIdForBridge(name, target.record) + target.record.copy( + value = prop, + type = ObjRecord.Type.Property, + methodId = methodId, + fieldId = null + ) + } + replaceMember(target, newRecord) + updateInstanceMember(target, newRecord) + } + + suspend fun commit() { + if (initHooks.isNotEmpty()) { + val target = cls.bridgeInitHooks ?: mutableListOf Unit>().also { + cls.bridgeInitHooks = it + } + target.addAll(initHooks) + val facade = instance.instanceScope.asFacade() + for (hook in initHooks) { + hook(facade, instance) + } + } + } + + private fun replaceMember(target: MemberTarget, newRecord: ObjRecord) { + when (target.kind) { + MemberKind.Instance -> { + cls.replaceMemberForBridge(target.name, newRecord) + if (target.mirrorClassScope && cls.classScope?.objects?.containsKey(target.name) == true) { + cls.replaceClassScopeMemberForBridge(target.name, newRecord) + } + } + MemberKind.Static -> cls.replaceClassScopeMemberForBridge(target.name, newRecord) + } + } + + private fun updateInstanceMember(target: MemberTarget, newRecord: ObjRecord) { + val key = instanceStorageKey(target, newRecord) ?: return + ensureInstanceSlotCapacity() + instance.instanceScope.objects[key] = newRecord + cls.fieldSlotForKey(key)?.let { slot -> + instance.setFieldSlotRecord(slot.slot, newRecord) + } + cls.methodSlotForKey(key)?.let { slot -> + instance.setMethodSlotRecord(slot.slot, newRecord) + } + } + + private fun instanceStorageKey(target: MemberTarget, rec: ObjRecord): String? = when (target.kind) { + MemberKind.Instance -> { + if (rec.visibility == Visibility.Private || + rec.type == ObjRecord.Type.Field || + rec.type == ObjRecord.Type.ConstructorField || + rec.type == ObjRecord.Type.Delegated) { + cls.mangledName(target.name) + } else { + target.name + } + } + MemberKind.Static -> { + if (rec.type != ObjRecord.Type.Fun && + rec.type != ObjRecord.Type.Property && + rec.type != ObjRecord.Type.Delegated) { + null + } else if (rec.visibility == Visibility.Private || rec.type == ObjRecord.Type.Delegated) { + cls.mangledName(target.name) + } else { + target.name + } + } + } + + private fun ensureInstanceSlotCapacity() { + val fieldCount = cls.fieldSlotCount() + if (instance.fieldSlots.size < fieldCount) { + val newSlots = arrayOfNulls(fieldCount) + instance.fieldSlots.copyInto(newSlots, 0, 0, instance.fieldSlots.size) + instance.fieldSlots = newSlots + } + val methodCount = cls.methodSlotCount() + if (instance.methodSlots.size < methodCount) { + val newSlots = arrayOfNulls(methodCount) + instance.methodSlots.copyInto(newSlots, 0, 0, instance.methodSlots.size) + instance.methodSlots = newSlots + } + } + + private fun findMember(name: String): MemberTarget { + val inst = cls.members[name] + val stat = cls.classScope?.objects?.get(name) + if (inst != null) { + return MemberTarget(name, inst, MemberKind.Instance, mirrorClassScope = stat != null) + } + if (stat != null) return MemberTarget(name, stat, MemberKind.Static) + throw ScriptError(Pos.builtIn, "extern member $name not found in class ${cls.className}") + } + + private fun removeFieldInitializersFor(name: String) { + if (cls.instanceInitializers.isEmpty()) return + val storageName = cls.mangledName(name) + cls.instanceInitializers.removeAll { init -> + val stmt = init as? Statement ?: return@removeAll false + val original = (stmt as? BytecodeStatement)?.original ?: stmt + original is InstanceFieldInitStatement && original.storageName == storageName + } + } +} + private suspend fun resolveClass( className: String, module: String?, @@ -349,3 +612,26 @@ private suspend fun resolveClass( } throw ScriptError(Pos.builtIn, "class $className not found in module ${scope.packageName}") } + +private suspend fun resolveObject( + objectName: String, + module: String?, + moduleScope: ModuleScope?, + importManager: ImportManager +): ObjInstance { + val scope = moduleScope ?: run { + if (module == null) { + throw ScriptError(Pos.builtIn, "module is required to resolve $objectName") + } + importManager.createModuleScope(Pos.builtIn, module) + } + val rec = scope.get(objectName) + val direct = rec?.value as? ObjInstance + if (direct != null) return direct + if (objectName.contains('.')) { + val resolved = scope.resolveQualifiedIdentifier(objectName) + val inst = resolved as? ObjInstance + if (inst != null) return inst + } + throw ScriptError(Pos.builtIn, "object $objectName not found in module ${scope.packageName}") +} diff --git a/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt b/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt index ae0eaff..ca44b87 100644 --- a/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt +++ b/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt @@ -1,13 +1,30 @@ +/* + * 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 kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue import kotlinx.coroutines.test.runTest import net.sergeych.lyng.bridge.LyngClassBridge +import net.sergeych.lyng.bridge.bindObject import net.sergeych.lyng.bridge.data import net.sergeych.lyng.obj.ObjInt import net.sergeych.lyng.obj.ObjString +import kotlin.test.Test +import kotlin.test.assertTrue class BridgeBindingTest { private data class CounterState(var count: Long) @@ -145,4 +162,55 @@ class BridgeBindingTest { } assertTrue(bindFailed) } + + @Test + fun testExternObjectBinding() = runTest { + val im = Script.defaultImportManager.copy() + im.addPackage("bridge.obj") { scope -> + scope.eval( + """ + extern object HostObject { + extern fun add(a: Int, b: Int): Int + extern val status: String + extern var count: Int + } + """.trimIndent() + ) + scope.bindObject("HostObject") { + classData = "OK" + init { _ -> + data = CounterState(5) + } + addFun("add") { _, _, args -> + val a = (args.list[0] as ObjInt).value + val b = (args.list[1] as ObjInt).value + ObjInt.of(a + b) + } + addVal("status") { _, _ -> ObjString(classData as String) } + addVar( + "count", + get = { _, instance -> + val st = (instance as net.sergeych.lyng.obj.ObjInstance).data as CounterState + ObjInt.of(st.count) + }, + set = { _, instance, value -> + val st = (instance as net.sergeych.lyng.obj.ObjInstance).data as CounterState + st.count = (value as ObjInt).value + } + ) + } + } + + val scope = im.newStdScope() + scope.eval( + """ + import bridge.obj + assertEquals(42, HostObject.add(10, 32)) + assertEquals("OK", HostObject.status) + assertEquals(5, HostObject.count) + HostObject.count = HostObject.count + 1 + assertEquals(6, HostObject.count) + """.trimIndent() + ) + } }