Kotlin bride: object bindings added

This commit is contained in:
Sergey Chernov 2026-02-19 08:12:28 +03:00
parent 45f3658742
commit e7c1adb2c5
2 changed files with 357 additions and 3 deletions

View File

@ -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. * Sugar for [LyngClassBridge.bind] on a module scope.
* *
@ -132,6 +177,16 @@ suspend fun ModuleScope.bind(
block: ClassBridgeBinder.() -> Unit block: ClassBridgeBinder.() -> Unit
): ObjClass = LyngClassBridge.bind(this, className, block) ): 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. */ /** Kotlin-side data slot attached to a Lyng instance. */
var ObjInstance.data: Any? var ObjInstance.data: Any?
get() = kotlinInstanceData 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<suspend (ScopeFacade, ObjInstance) -> 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<Obj>(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<suspend (ScopeFacade, ObjInstance) -> 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<ObjRecord>(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<ObjRecord>(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( private suspend fun resolveClass(
className: String, className: String,
module: String?, module: String?,
@ -349,3 +612,26 @@ private suspend fun resolveClass(
} }
throw ScriptError(Pos.builtIn, "class $className not found in module ${scope.packageName}") 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}")
}

View File

@ -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 package net.sergeych.lyng
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.bridge.LyngClassBridge import net.sergeych.lyng.bridge.LyngClassBridge
import net.sergeych.lyng.bridge.bindObject
import net.sergeych.lyng.bridge.data import net.sergeych.lyng.bridge.data
import net.sergeych.lyng.obj.ObjInt import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjString import net.sergeych.lyng.obj.ObjString
import kotlin.test.Test
import kotlin.test.assertTrue
class BridgeBindingTest { class BridgeBindingTest {
private data class CounterState(var count: Long) private data class CounterState(var count: Long)
@ -145,4 +162,55 @@ class BridgeBindingTest {
} }
assertTrue(bindFailed) 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()
)
}
} }