Add Kotlin bridge reflection handles and tests

This commit is contained in:
Sergey Chernov 2026-02-15 01:23:13 +03:00
parent cd145f6a96
commit 9cf87d1075
10 changed files with 1339 additions and 0 deletions

View File

@ -73,6 +73,9 @@ internal class ScopeBridge(internal val scope: Scope) : ScopeFacade {
override fun trace(text: String) = scope.trace(text) override fun trace(text: String) = scope.trace(text)
} }
/** Public factory for bridge facades. */
fun Scope.asFacade(): ScopeFacade = ScopeBridge(this)
inline fun <reified T : Obj> ScopeFacade.requiredArg(index: Int): T { inline fun <reified T : Obj> ScopeFacade.requiredArg(index: Int): T {
if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}") if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}")
return (args.list[index].byValueCopy() as? T) return (args.list[index].byValueCopy() as? T)

View File

@ -0,0 +1,533 @@
/*
* Kotlin bridge reflection facade: handle-based access for fast get/set/call.
*/
package net.sergeych.lyng.bridge
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeFacade
import net.sergeych.lyng.canAccessMember
import net.sergeych.lyng.extensionCallableName
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjIllegalAccessException
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjProperty
import net.sergeych.lyng.obj.ObjRecord
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.obj.ObjUnset
import net.sergeych.lyng.obj.ObjVoid
import net.sergeych.lyng.requireScope
import net.sergeych.lyng.ModuleScope
/** Where to resolve names from. */
enum class LookupTarget {
CurrentFrame,
ParentChain,
ModuleFrame
}
/** Explicit receiver view (like this@Base). */
data class ReceiverView(
val type: ObjClass? = null,
val typeName: String? = null
)
/** Lookup rules for bridge resolution. */
data class LookupSpec(
val targets: Set<LookupTarget> = setOf(LookupTarget.CurrentFrame, LookupTarget.ModuleFrame),
val receiverView: ReceiverView? = null
)
/** Base handle type. */
sealed interface BridgeHandle {
val name: String
}
/** Read-only value handle. */
interface ValHandle : BridgeHandle {
suspend fun get(scope: ScopeFacade): Obj
}
/** Read/write value handle. */
interface VarHandle : ValHandle {
suspend fun set(scope: ScopeFacade, value: Obj)
}
/** Callable handle (function/closure/method). */
interface CallableHandle : BridgeHandle {
suspend fun call(scope: ScopeFacade, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Obj
}
/** Member handle resolved against an instance or receiver view. */
interface MemberHandle : BridgeHandle {
val declaringClass: ObjClass?
val receiverView: ReceiverView?
}
/** Member field/property. */
interface MemberValHandle : MemberHandle, ValHandle
/** Member var/property with write access. */
interface MemberVarHandle : MemberHandle, VarHandle
/** Member callable (method or extension). */
interface MemberCallableHandle : MemberHandle, CallableHandle
/** Direct record handle (debug/inspection). */
interface RecordHandle : BridgeHandle {
fun record(): ObjRecord
}
/** Bridge resolver API (entry point for Kotlin bindings). */
interface BridgeResolver {
val pos: Pos
fun selfAs(type: ObjClass): BridgeResolver
fun selfAs(typeName: String): BridgeResolver
fun resolveVal(name: String, lookup: LookupSpec = LookupSpec()): ValHandle
fun resolveVar(name: String, lookup: LookupSpec = LookupSpec()): VarHandle
fun resolveCallable(name: String, lookup: LookupSpec = LookupSpec()): CallableHandle
fun resolveMemberVal(
receiver: Obj,
name: String,
lookup: LookupSpec = LookupSpec()
): MemberValHandle
fun resolveMemberVar(
receiver: Obj,
name: String,
lookup: LookupSpec = LookupSpec()
): MemberVarHandle
fun resolveMemberCallable(
receiver: Obj,
name: String,
lookup: LookupSpec = LookupSpec()
): MemberCallableHandle
/** Extension function treated as a member for reflection. */
fun resolveExtensionCallable(
receiverClass: ObjClass,
name: String,
lookup: LookupSpec = LookupSpec()
): MemberCallableHandle
/** Debug: resolve locals by name (optional, for tooling). */
fun resolveLocalVal(name: String): ValHandle
fun resolveLocalVar(name: String): VarHandle
/** Debug: access raw record handles if needed. */
fun resolveRecord(name: String, lookup: LookupSpec = LookupSpec()): RecordHandle
}
/** Convenience: call by name with implicit caching in resolver implementation. */
interface BridgeCallByName {
suspend fun callByName(
scope: ScopeFacade,
name: String,
args: Arguments = Arguments.EMPTY,
lookup: LookupSpec = LookupSpec()
): Obj
}
/** Optional typed wrappers (sugar). */
interface TypedHandle<T : Obj> : ValHandle {
suspend fun getTyped(scope: ScopeFacade): T
}
/** Factory for bridge resolver. */
fun ScopeFacade.resolver(): BridgeResolver = BridgeResolverImpl(this)
private class BridgeResolverImpl(
private val facade: ScopeFacade,
private val receiverView: ReceiverView? = null
) : BridgeResolver, BridgeCallByName {
private val cachedCallables: MutableMap<String, CallableHandle> = LinkedHashMap()
override val pos: Pos
get() = facade.pos
override fun selfAs(type: ObjClass): BridgeResolver = BridgeResolverImpl(facade, ReceiverView(type = type))
override fun selfAs(typeName: String): BridgeResolver = BridgeResolverImpl(facade, ReceiverView(typeName = typeName))
override fun resolveVal(name: String, lookup: LookupSpec): ValHandle =
LocalValHandle(this, name, lookup)
override fun resolveVar(name: String, lookup: LookupSpec): VarHandle =
LocalVarHandle(this, name, lookup)
override fun resolveCallable(name: String, lookup: LookupSpec): CallableHandle =
LocalCallableHandle(this, name, lookup)
override fun resolveMemberVal(receiver: Obj, name: String, lookup: LookupSpec): MemberValHandle =
MemberValHandleImpl(this, receiver, name, lookup.receiverView ?: receiverView)
override fun resolveMemberVar(receiver: Obj, name: String, lookup: LookupSpec): MemberVarHandle =
MemberVarHandleImpl(this, receiver, name, lookup.receiverView ?: receiverView)
override fun resolveMemberCallable(receiver: Obj, name: String, lookup: LookupSpec): MemberCallableHandle =
MemberCallableHandleImpl(this, receiver, name, lookup.receiverView ?: receiverView)
override fun resolveExtensionCallable(receiverClass: ObjClass, name: String, lookup: LookupSpec): MemberCallableHandle =
ExtensionCallableHandleImpl(this, receiverClass, name, lookup)
override fun resolveLocalVal(name: String): ValHandle =
LocalValHandle(this, name, LookupSpec(targets = setOf(LookupTarget.CurrentFrame)))
override fun resolveLocalVar(name: String): VarHandle =
LocalVarHandle(this, name, LookupSpec(targets = setOf(LookupTarget.CurrentFrame)))
override fun resolveRecord(name: String, lookup: LookupSpec): RecordHandle =
RecordHandleImpl(this, name, lookup)
override suspend fun callByName(scope: ScopeFacade, name: String, args: Arguments, lookup: LookupSpec): Obj {
val handle = cachedCallables.getOrPut(name) { resolveCallable(name, lookup) }
return handle.call(scope, args)
}
fun facade(): ScopeFacade = facade
fun resolveLocalRecord(scope: Scope, name: String, lookup: LookupSpec): ObjRecord {
val caller = scope.currentClassCtx
if (LookupTarget.CurrentFrame in lookup.targets) {
scope.tryGetLocalRecord(scope, name, caller)?.let { return it }
}
if (LookupTarget.ParentChain in lookup.targets) {
scope.chainLookupIgnoreClosure(name, followClosure = false, caller = caller)?.let { return it }
}
if (LookupTarget.ModuleFrame in lookup.targets) {
findModuleScope(scope)?.let { module ->
module.tryGetLocalRecord(module, name, caller)?.let { return it }
}
}
facade.raiseSymbolNotFound(name)
}
fun resolveReceiver(scope: Scope, receiver: Obj, view: ReceiverView?): Obj {
if (view == null) return receiver
if (receiver !== scope.thisObj) return receiver
val target = when {
view.type != null -> scope.thisVariants.firstOrNull { it.isInstanceOf(view.type) }
view.typeName != null -> scope.thisVariants.firstOrNull { it.isInstanceOf(view.typeName) }
else -> null
}
return target ?: facade.raiseSymbolNotFound(view.typeName ?: view.type?.className ?: "<receiver>")
}
fun resolveMemberRecord(scope: Scope, receiver: Obj, name: String): MemberResolution {
if (receiver is ObjClass) {
val rec = receiver.classScope?.objects?.get(name) ?: receiver.members[name]
?: facade.raiseSymbolNotFound("member $name not found on ${receiver.className}")
val decl = rec.declaringClass ?: receiver
if (!canAccessMember(rec.visibility, decl, scope.currentClassCtx, name)) {
facade.raiseError(
ObjIllegalAccessException(
scope,
"can't access ${name}: not visible (declared in ${decl.className}, caller ${scope.currentClassCtx?.className ?: "?"})"
)
)
}
return MemberResolution(rec, decl, receiver, rec.fieldId, rec.methodId)
}
val cls = receiver.objClass
val resolved = cls.resolveInstanceMember(name)
?: facade.raiseSymbolNotFound("member $name not found on ${cls.className}")
val decl = resolved.declaringClass
if (!canAccessMember(resolved.record.visibility, decl, scope.currentClassCtx, name)) {
facade.raiseError(
ObjIllegalAccessException(
scope,
"can't access ${name}: not visible (declared in ${decl.className}, caller ${scope.currentClassCtx?.className ?: "?"})"
)
)
}
val fieldId = if (resolved.record.type == ObjRecord.Type.Field ||
resolved.record.type == ObjRecord.Type.ConstructorField
) {
resolved.record.fieldId ?: cls.instanceFieldIdMap()[name]
} else null
val methodId = if (resolved.record.type == ObjRecord.Type.Fun ||
resolved.record.type == ObjRecord.Type.Property ||
resolved.record.type == ObjRecord.Type.Delegated
) {
resolved.record.methodId ?: cls.instanceMethodIdMap(includeAbstract = true)[name]
} else null
return MemberResolution(resolved.record, decl, receiver.objClass, fieldId, methodId)
}
private fun findModuleScope(scope: Scope): ModuleScope? {
var s: Scope? = scope
var hops = 0
while (s != null && hops++ < 1024) {
if (s is ModuleScope) return s
s = s.parent
}
return null
}
}
private data class LocalResolution(
val record: ObjRecord,
val frameId: Long
)
private data class MemberResolution(
val record: ObjRecord,
val declaringClass: ObjClass,
val receiverClass: ObjClass,
val fieldId: Int?,
val methodId: Int?
)
private abstract class LocalHandleBase(
protected val resolver: BridgeResolverImpl,
override val name: String,
private val lookup: LookupSpec
) : BridgeHandle {
private var cached: LocalResolution? = null
protected fun resolve(scope: Scope): ObjRecord {
val cachedLocal = cached
if (cachedLocal != null && cachedLocal.frameId == scope.frameId) {
return cachedLocal.record
}
val rec = resolver.resolveLocalRecord(scope, name, lookup)
cached = LocalResolution(rec, scope.frameId)
return rec
}
}
private class LocalValHandle(
resolver: BridgeResolverImpl,
name: String,
lookup: LookupSpec
) : LocalHandleBase(resolver, name, lookup), ValHandle {
override suspend fun get(scope: ScopeFacade): Obj {
val real = scope.requireScope()
val rec = resolve(real)
return real.resolve(rec, name)
}
}
private class LocalVarHandle(
resolver: BridgeResolverImpl,
name: String,
lookup: LookupSpec
) : LocalHandleBase(resolver, name, lookup), VarHandle {
override suspend fun get(scope: ScopeFacade): Obj {
val real = scope.requireScope()
val rec = resolve(real)
return real.resolve(rec, name)
}
override suspend fun set(scope: ScopeFacade, value: Obj) {
val real = scope.requireScope()
val rec = resolve(real)
real.assign(rec, name, value)
}
}
private class LocalCallableHandle(
resolver: BridgeResolverImpl,
name: String,
lookup: LookupSpec
) : LocalHandleBase(resolver, name, lookup), CallableHandle {
override suspend fun call(scope: ScopeFacade, args: Arguments, newThisObj: Obj?): Obj {
val real = scope.requireScope()
val rec = resolve(real)
val callee = rec.value
return scope.call(callee, args, newThisObj ?: rec.receiver)
}
}
private abstract class MemberHandleBase(
protected val resolver: BridgeResolverImpl,
receiver: Obj,
override val name: String,
override val receiverView: ReceiverView?
) : MemberHandle {
private val baseReceiver: Obj = receiver
private var cachedResolution: MemberResolution? = null
private var cachedDeclaringClass: ObjClass? = null
protected fun resolve(scope: Scope): Pair<Obj, MemberResolution> {
val resolvedReceiver = resolver.resolveReceiver(scope, baseReceiver, receiverView)
val cached = cachedResolution
if (cached != null && resolvedReceiver.objClass === cached.receiverClass) {
cachedDeclaringClass = cached.declaringClass
return Pair(resolvedReceiver, cached)
}
val res = resolver.resolveMemberRecord(scope, resolvedReceiver, name)
cachedResolution = res
cachedDeclaringClass = res.declaringClass
return Pair(resolvedReceiver, res)
}
protected fun declaringClass(): ObjClass? = cachedDeclaringClass
}
private class MemberValHandleImpl(
resolver: BridgeResolverImpl,
receiver: Obj,
name: String,
receiverView: ReceiverView?
) : MemberHandleBase(resolver, receiver, name, receiverView), MemberValHandle {
override val declaringClass: ObjClass?
get() = declaringClass()
override suspend fun get(scope: ScopeFacade): Obj {
val real = scope.requireScope()
val (receiver, res) = resolve(real)
val rec = resolveMemberRecordFast(receiver, res)
return receiver.resolveRecord(real, rec, name, res.declaringClass).value
}
}
private class MemberVarHandleImpl(
resolver: BridgeResolverImpl,
receiver: Obj,
name: String,
receiverView: ReceiverView?
) : MemberHandleBase(resolver, receiver, name, receiverView), MemberVarHandle {
override val declaringClass: ObjClass?
get() = declaringClass()
override suspend fun get(scope: ScopeFacade): Obj {
val real = scope.requireScope()
val (receiver, res) = resolve(real)
val rec = resolveMemberRecordFast(receiver, res)
return receiver.resolveRecord(real, rec, name, res.declaringClass).value
}
override suspend fun set(scope: ScopeFacade, value: Obj) {
val real = scope.requireScope()
val (receiver, res) = resolve(real)
val rec = resolveMemberRecordFast(receiver, res)
assignMemberRecord(real, receiver, res.declaringClass, rec, name, value)
}
}
private class MemberCallableHandleImpl(
resolver: BridgeResolverImpl,
receiver: Obj,
name: String,
receiverView: ReceiverView?
) : MemberHandleBase(resolver, receiver, name, receiverView), MemberCallableHandle {
override val declaringClass: ObjClass?
get() = declaringClass()
override suspend fun call(scope: ScopeFacade, args: Arguments, newThisObj: Obj?): Obj {
val real = scope.requireScope()
val (receiver, res) = resolve(real)
val rec = resolveMemberRecordFast(receiver, res)
if (rec.type != ObjRecord.Type.Fun) {
scope.raiseError("member $name is not callable")
}
return rec.value.invoke(real, receiver, args, res.declaringClass)
}
}
private class ExtensionCallableHandleImpl(
private val resolver: BridgeResolverImpl,
private val receiverClass: ObjClass,
override val name: String,
private val lookup: LookupSpec
) : MemberCallableHandle {
override val receiverView: ReceiverView?
get() = null
override val declaringClass: ObjClass?
get() = receiverClass
override suspend fun call(scope: ScopeFacade, args: Arguments, newThisObj: Obj?): Obj {
val real = scope.requireScope()
val wrapperName = extensionCallableName(receiverClass.className, name)
val rec = resolver.resolveLocalRecord(real, wrapperName, lookup)
val receiver = newThisObj ?: real.thisObj
val callArgs = Arguments(listOf(receiver) + args.list)
return scope.call(rec.value, callArgs)
}
}
private class RecordHandleImpl(
private val resolver: BridgeResolverImpl,
override val name: String,
private val lookup: LookupSpec
) : RecordHandle {
override fun record(): ObjRecord {
val scope = resolver.facade().requireScope()
return resolver.resolveLocalRecord(scope, name, lookup)
}
}
private class TypedHandleImpl<T : Obj>(
private val inner: ValHandle,
private val clazzName: String
) : TypedHandle<T> {
override val name: String
get() = inner.name
override suspend fun get(scope: ScopeFacade): Obj = inner.get(scope)
@Suppress("UNCHECKED_CAST")
override suspend fun getTyped(scope: ScopeFacade): T {
val value = inner.get(scope)
return (value as? T)
?: scope.raiseClassCastError("Expected $clazzName, got ${value.objClass.className}")
}
}
private fun resolveMemberRecordFast(receiver: Obj, res: MemberResolution): ObjRecord {
val inst = receiver as? ObjInstance
if (inst != null) {
res.fieldId?.let { inst.fieldRecordForId(it)?.let { return it } }
res.methodId?.let { inst.methodRecordForId(it)?.let { return it } }
}
return res.record
}
private suspend fun assignMemberRecord(
scope: Scope,
receiver: Obj,
declaringClass: ObjClass,
rec: ObjRecord,
name: String,
value: Obj
) {
val caller = scope.currentClassCtx
if (!canAccessMember(rec.effectiveWriteVisibility, declaringClass, caller, name)) {
scope.raiseError(
ObjIllegalAccessException(
scope,
"can't assign ${name}: not visible (declared in ${declaringClass.className}, caller ${caller?.className ?: "?"})"
)
)
}
when {
rec.type == ObjRecord.Type.Delegated -> {
val del = rec.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
val th = if (receiver === ObjVoid) net.sergeych.lyng.obj.ObjNull else receiver
del.invokeInstanceMethod(scope, "setValue", Arguments(th, ObjString(name), value))
}
rec.value is ObjProperty || rec.type == ObjRecord.Type.Property -> {
val prop = rec.value as? ObjProperty
?: scope.raiseError("Expected ObjProperty for property member $name")
prop.callSetter(scope, receiver, value, declaringClass)
}
rec.isMutable -> {
val slotRef = rec.value
if (slotRef is net.sergeych.lyng.FrameSlotRef) {
if (!rec.isMutable && slotRef.read() !== ObjUnset) scope.raiseError("can't reassign val $name")
slotRef.write(value)
} else {
rec.value = value
}
}
else -> scope.raiseError("can't assign to read-only field: $name")
}
}

View File

@ -0,0 +1,286 @@
/*
* Kotlin bridge bindings for Lyng classes (Lyng-first workflow).
*/
package net.sergeych.lyng.bridge
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Pos
import net.sergeych.lyng.ScopeFacade
import net.sergeych.lyng.Script
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjExternCallable
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjProperty
import net.sergeych.lyng.obj.ObjRecord
import net.sergeych.lyng.obj.ObjVoid
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.requiredArg
import net.sergeych.lyng.InstanceFieldInitStatement
import net.sergeych.lyng.Statement
import net.sergeych.lyng.bytecode.BytecodeStatement
interface BridgeInstanceContext {
val instance: Obj
var data: Any?
}
interface ClassBridgeBinder {
var classData: Any?
fun init(block: suspend BridgeInstanceContext.(ScopeFacade) -> Unit)
fun initWithInstance(block: suspend (ScopeFacade, Obj) -> Unit)
fun addFun(name: String, impl: suspend (ScopeFacade, Obj, Arguments) -> Obj)
fun addVal(name: String, impl: suspend (ScopeFacade, Obj) -> Obj)
fun addVar(
name: String,
get: suspend (ScopeFacade, Obj) -> Obj,
set: suspend (ScopeFacade, Obj, Obj) -> Unit
)
}
object LyngClassBridge {
suspend fun bind(
className: String,
module: String? = null,
importManager: ImportManager = Script.defaultImportManager,
block: ClassBridgeBinder.() -> Unit
): ObjClass {
val cls = resolveClass(className, module, null, importManager)
return bind(cls, block)
}
suspend fun bind(
moduleScope: ModuleScope,
className: String,
block: ClassBridgeBinder.() -> Unit
): ObjClass {
val cls = resolveClass(className, null, moduleScope, Script.defaultImportManager)
return bind(cls, block)
}
fun bind(clazz: ObjClass, block: ClassBridgeBinder.() -> Unit): ObjClass {
val binder = ClassBridgeBinderImpl(clazz)
binder.block()
binder.commit()
return clazz
}
}
var ObjInstance.data: Any?
get() = kotlinInstanceData
set(value) { kotlinInstanceData = value }
var ObjClass.classData: Any?
get() = kotlinClassData
set(value) { kotlinClassData = value }
private enum class MemberKind { Instance, Static }
private data class MemberTarget(
val name: String,
val record: ObjRecord,
val kind: MemberKind,
val mirrorClassScope: Boolean = false
)
private class BridgeInstanceContextImpl(
override val instance: Obj
) : BridgeInstanceContext {
private fun instanceObj(): ObjInstance =
instance as? ObjInstance ?: error("Bridge instance is not an ObjInstance")
override var data: Any?
get() = instanceObj().kotlinInstanceData
set(value) { instanceObj().kotlinInstanceData = value }
}
private class ClassBridgeBinderImpl(
private val cls: ObjClass
) : ClassBridgeBinder {
private val initHooks = mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>()
private var checkedTemplate = false
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) {
ensureTemplateNotBuilt()
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)
}
override fun addVal(name: String, impl: suspend (ScopeFacade, Obj) -> Obj) {
ensureTemplateNotBuilt()
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)
}
override fun addVar(
name: String,
get: suspend (ScopeFacade, Obj) -> Obj,
set: suspend (ScopeFacade, Obj, Obj) -> Unit
) {
ensureTemplateNotBuilt()
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)
}
fun commit() {
if (initHooks.isNotEmpty()) {
val target = cls.bridgeInitHooks ?: mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>().also {
cls.bridgeInitHooks = it
}
target.addAll(initHooks)
}
}
private fun ensureTemplateNotBuilt() {
if (!checkedTemplate) {
if (cls.instanceTemplateBuilt) {
throw ScriptError(
Pos.builtIn,
"bridge binding for ${cls.className} must happen before first instance is created"
)
}
checkedTemplate = true
}
}
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 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?,
moduleScope: ModuleScope?,
importManager: ImportManager
): ObjClass {
val scope = moduleScope ?: run {
if (module == null) {
throw ScriptError(Pos.builtIn, "module is required to resolve $className")
}
importManager.createModuleScope(Pos.builtIn, module)
}
val rec = scope.get(className)
val direct = rec?.value as? ObjClass
if (direct != null) return direct
if (className.contains('.')) {
val resolved = scope.resolveQualifiedIdentifier(className)
val cls = resolved as? ObjClass
if (cls != null) return cls
}
throw ScriptError(Pos.builtIn, "class $className not found in module ${scope.packageName}")
}

View File

@ -289,6 +289,14 @@ open class ObjClass(
private var methodSlotMap: Map<String, MethodSlot> = emptyMap() private var methodSlotMap: Map<String, MethodSlot> = emptyMap()
private var methodSlotCount: Int = 0 private var methodSlotCount: Int = 0
/** Kotlin bridge class-level storage (no name lookup). */
internal var kotlinClassData: Any? = null
/** Kotlin bridge instance init hooks. */
internal var bridgeInitHooks: MutableList<suspend (net.sergeych.lyng.ScopeFacade, ObjInstance) -> Unit>? = null
internal var instanceTemplateBuilt: Boolean = false
private fun ensureFieldSlots(): Map<String, FieldSlot> { private fun ensureFieldSlots(): Map<String, FieldSlot> {
if (fieldSlotLayoutVersion == layoutVersion) return fieldSlotMap if (fieldSlotLayoutVersion == layoutVersion) return fieldSlotMap
val res = mutableMapOf<String, FieldSlot>() val res = mutableMapOf<String, FieldSlot>()
@ -480,12 +488,20 @@ open class ObjClass(
val existingId = rec.methodId val existingId = rec.methodId
if (existingId != null) { if (existingId != null) {
methodIdMap[name] = existingId methodIdMap[name] = existingId
if (existingId >= nextMethodId) {
nextMethodId = existingId + 1
}
return existingId return existingId
} }
val id = methodIdMap.getOrPut(name) { nextMethodId++ } val id = methodIdMap.getOrPut(name) { nextMethodId++ }
if (id >= nextMethodId) {
nextMethodId = id + 1
}
return id return id
} }
internal fun ensureMethodIdForBridge(name: String, rec: ObjRecord): Int = assignMethodId(name, rec)
private fun ensureMethodIdSeeded() { private fun ensureMethodIdSeeded() {
if (methodIdSeeded) return if (methodIdSeeded) return
var maxId = -1 var maxId = -1
@ -515,6 +531,17 @@ open class ObjClass(
methodIdSeeded = true methodIdSeeded = true
} }
internal fun replaceMemberForBridge(name: String, newRecord: ObjRecord) {
members[name] = newRecord
layoutVersion += 1
}
internal fun replaceClassScopeMemberForBridge(name: String, newRecord: ObjRecord) {
initClassScope()
classScope!!.objects[name] = newRecord
layoutVersion += 1
}
override fun toString(): String = className override fun toString(): String = className
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1 override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
@ -552,6 +579,7 @@ open class ObjClass(
} }
} }
} }
instanceTemplateBuilt = true
res res
} }
@ -634,6 +662,14 @@ open class ObjClass(
args: Arguments?, args: Arguments?,
runConstructors: Boolean runConstructors: Boolean
) { ) {
bridgeInitHooks?.let { hooks ->
if (hooks.isNotEmpty()) {
val facade = instance.instanceScope.asFacade()
for (hook in hooks) {
hook(facade, instance)
}
}
}
val visited = hashSetOf<ObjClass>() val visited = hashSetOf<ObjClass>()
initClassInternal(instance, visited, this, args, isRoot = true, runConstructors = runConstructors) initClassInternal(instance, visited, this, args, isRoot = true, runConstructors = runConstructors)
} }

View File

@ -29,6 +29,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
internal lateinit var instanceScope: Scope internal lateinit var instanceScope: Scope
internal var fieldSlots: Array<ObjRecord?> = emptyArray() internal var fieldSlots: Array<ObjRecord?> = emptyArray()
internal var methodSlots: Array<ObjRecord?> = emptyArray() internal var methodSlots: Array<ObjRecord?> = emptyArray()
internal var kotlinInstanceData: Any? = null
internal fun initFieldSlots(size: Int) { internal fun initFieldSlots(size: Int) {
fieldSlots = arrayOfNulls(size) fieldSlots = arrayOfNulls(size)

View File

@ -0,0 +1,148 @@
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.data
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjString
class BridgeBindingTest {
private data class CounterState(var count: Long)
@Test
fun testExternClassBinding() = runTest {
val im = Script.defaultImportManager.copy()
im.addPackage("bridge.mod") { scope ->
scope.eval(
"""
class Foo {
extern fun add(a: Int, b: Int): Int
extern val status: String
extern var count: Int
private extern fun secret(): Int
static extern fun ping(): Int
fun callAdd() = add(2, 3)
fun callSecret() = secret()
fun bump() { count = count + 1 }
}
class Bar {
extern var count: Int
fun inc() { count = count + 1 }
}
""".trimIndent()
)
}
LyngClassBridge.bind(className = "Foo", module = "bridge.mod", importManager = im) {
classData = "OK"
init { _ ->
data = CounterState(0)
}
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
}
)
addFun("secret") { _, _, _ -> ObjInt.of(42) }
addFun("ping") { _, _, _ -> ObjInt.of(7) }
}
LyngClassBridge.bind(className = "Bar", module = "bridge.mod", importManager = im) {
initWithInstance { _, instance ->
(instance as net.sergeych.lyng.obj.ObjInstance).data = CounterState(10)
}
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.mod
val f = Foo()
assertEquals(5, f.callAdd())
assertEquals("OK", f.status)
assertEquals(0, f.count)
f.bump()
assertEquals(1, f.count)
assertEquals(42, f.callSecret())
assertEquals(7, Foo.ping())
val b = Bar()
assertEquals(10, b.count)
b.inc()
assertEquals(11, b.count)
""".trimIndent()
)
val privateCallFails = try {
scope.eval(
"""
import bridge.mod
Foo().secret()
""".trimIndent()
)
false
} catch (_: ScriptError) {
true
}
assertTrue(privateCallFails)
}
@Test
fun testBindAfterInstanceFails() = runTest {
val im = Script.defaultImportManager.copy()
im.addPackage("bridge.late") { scope ->
scope.eval(
"""
class Late {
extern val status: String
}
""".trimIndent()
)
}
val scope = im.newStdScope()
scope.eval(
"""
import bridge.late
val l = Late()
""".trimIndent()
)
val bindFailed = try {
LyngClassBridge.bind(className = "Late", module = "bridge.late", importManager = im) {
addVal("status") { _, _ -> ObjString("late") }
}
false
} catch (_: ScriptError) {
true
}
assertTrue(bindFailed)
}
}

View File

@ -0,0 +1,54 @@
package net.sergeych.lyng
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.bridge.BridgeCallByName
import net.sergeych.lyng.bridge.resolver
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjInt
class BridgeResolverTest {
@Test
fun testLocalAndMemberHandles() = runTest {
val im = Script.defaultImportManager.copy()
val scope = im.newStdScope()
scope.eval(
"""
var x = 1
fun add(a: Int, b: Int) = a + b
class Foo {
var count: Int = 0
fun bump() { count = count + 1 }
}
val f = Foo()
""".trimIndent()
)
val facade = scope.asFacade()
val resolver = facade.resolver()
val xHandle = resolver.resolveVar("x")
assertEquals(1, (xHandle.get(facade) as ObjInt).value)
xHandle.set(facade, ObjInt.of(5))
assertEquals(5, (xHandle.get(facade) as ObjInt).value)
val addHandle = resolver.resolveCallable("add")
val addResult = addHandle.call(facade, Arguments(ObjInt.of(2), ObjInt.of(3)))
assertEquals(5, (addResult as ObjInt).value)
val fRec = scope["f"] ?: error("f not found")
val fObj = scope.resolve(fRec, "f") as ObjInstance
val countHandle = resolver.resolveMemberVar(fObj, "count")
assertEquals(0, (countHandle.get(facade) as ObjInt).value)
val bumpHandle = resolver.resolveMemberCallable(fObj, "bump")
bumpHandle.call(facade)
assertEquals(1, (countHandle.get(facade) as ObjInt).value)
val callByName = resolver as BridgeCallByName
val addByName = callByName.callByName(facade, "add", Arguments(ObjInt.of(4), ObjInt.of(6)))
assertEquals(10, (addByName as ObjInt).value)
}
}

View File

@ -0,0 +1,164 @@
/*
* Draft: Kotlin bridge facade + handle API (declarations only)
* For discussion; not wired into runtime yet.
*/
package net.sergeych.lyng.bridge
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.ObjClass
import net.sergeych.lyng.Pos
import net.sergeych.lyng.ScopeFacade
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjRecord
/** Where to resolve names from. */
enum class LookupTarget {
CurrentFrame,
ParentChain,
ModuleFrame
}
/** Explicit receiver view (like this@Base). */
data class ReceiverView(
val type: ObjClass? = null,
val typeName: String? = null
)
/** Lookup rules for bridge resolution. */
data class LookupSpec(
val targets: Set<LookupTarget> = setOf(LookupTarget.CurrentFrame, LookupTarget.ModuleFrame),
val receiverView: ReceiverView? = null
)
/** Base handle type. */
sealed interface BridgeHandle {
val name: String
}
/** Read-only value handle. */
interface ValHandle : BridgeHandle {
suspend fun get(scope: ScopeFacade): Obj
}
/** Read/write value handle. */
interface VarHandle : ValHandle {
suspend fun set(scope: ScopeFacade, value: Obj)
}
/** Callable handle (function/closure/method). */
interface CallableHandle : BridgeHandle {
suspend fun call(scope: ScopeFacade, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Obj
}
/** Member handle resolved against an instance or receiver view. */
interface MemberHandle : BridgeHandle {
val declaringClass: ObjClass?
val receiverView: ReceiverView?
}
/** Member field/property. */
interface MemberValHandle : MemberHandle, ValHandle
/** Member var/property with write access. */
interface MemberVarHandle : MemberHandle, VarHandle
/** Member callable (method or extension). */
interface MemberCallableHandle : MemberHandle, CallableHandle
/** Direct record handle (debug/inspection). */
interface RecordHandle : BridgeHandle {
fun record(): ObjRecord
}
/** Bridge resolver API (entry point for Kotlin bindings). */
interface BridgeResolver {
val pos: Pos
fun selfAs(type: ObjClass): BridgeResolver
fun selfAs(typeName: String): BridgeResolver
fun resolveVal(name: String, lookup: LookupSpec = LookupSpec()): ValHandle
fun resolveVar(name: String, lookup: LookupSpec = LookupSpec()): VarHandle
fun resolveCallable(name: String, lookup: LookupSpec = LookupSpec()): CallableHandle
fun resolveMemberVal(
receiver: Obj,
name: String,
lookup: LookupSpec = LookupSpec()
): MemberValHandle
fun resolveMemberVar(
receiver: Obj,
name: String,
lookup: LookupSpec = LookupSpec()
): MemberVarHandle
fun resolveMemberCallable(
receiver: Obj,
name: String,
lookup: LookupSpec = LookupSpec()
): MemberCallableHandle
/** Extension function treated as a member for reflection. */
fun resolveExtensionCallable(
receiverClass: ObjClass,
name: String,
lookup: LookupSpec = LookupSpec()
): MemberCallableHandle
/** Debug: resolve locals by name (optional, for tooling). */
fun resolveLocalVal(name: String): ValHandle
fun resolveLocalVar(name: String): VarHandle
/** Debug: access raw record handles if needed. */
fun resolveRecord(name: String, lookup: LookupSpec = LookupSpec()): RecordHandle
}
/** Convenience: call by name with implicit caching in resolver implementation. */
interface BridgeCallByName {
suspend fun callByName(
scope: ScopeFacade,
name: String,
args: Arguments = Arguments.EMPTY,
lookup: LookupSpec = LookupSpec()
): Obj
}
/** Optional typed wrappers (sugar). */
interface TypedHandle<T : Obj> : ValHandle {
suspend fun getTyped(scope: ScopeFacade): T
}
/** Debug view of frame stack for tooling. */
interface FrameStackView {
fun frames(): List<FrameView>
}
interface FrameView {
val pos: Pos
val thisObj: Obj
fun locals(): Map<String, ObjRecord>
}
/** Binder API for Kotlin -> Lyng extern wiring. */
interface ClassBridgeBinder {
var classData: Any?
fun init(block: suspend BridgeInstanceContext.(ScopeFacade) -> Unit)
fun initWithInstance(block: suspend (ScopeFacade, Obj) -> Unit)
fun addFun(name: String, impl: suspend (ScopeFacade, Obj, Arguments) -> Obj)
fun addVal(name: String, impl: suspend (ScopeFacade, Obj) -> Obj)
fun addVar(
name: String,
get: suspend (ScopeFacade, Obj) -> Obj,
set: suspend (ScopeFacade, Obj, Obj) -> Unit
)
}
/** Instance receiver context for init/impl blocks. */
interface BridgeInstanceContext {
val instance: Obj
var data: Any?
}

View File

@ -0,0 +1,72 @@
# Kotlin Bridge: Lyng-First Class Binding
This note describes the Lyng-first workflow where a class is declared in Lyng and Kotlin provides extern implementations.
## Overview
- Lyng code declares a class and marks members as `extern`.
- Kotlin binds implementations with `LyngClassBridge.bind(...)`.
- Binding must happen **before the first instance is created**.
- `bind(className, module, importManager)` requires `module` to resolve class names; use
`bind(moduleScope, className, ...)` or `bind(ObjClass, ...)` if you already have a scope/class.
- Kotlin can store two opaque payloads:
- `instance.data` (per instance)
- `classData` (per class)
## Lyng: declare extern members
```lyng
class Foo {
extern fun add(a: Int, b: Int): Int
extern val status: String
extern var count: Int
private extern fun secret(): Int
static extern fun ping(): Int
fun callAdd() = add(2, 3)
fun callSecret() = secret()
fun bump() { count = count + 1 }
}
```
## Kotlin: bind extern implementations
```kotlin
LyngClassBridge.bind(className = "Foo", module = "bridge.mod", importManager = im) {
classData = "OK"
init { _ ->
data = CounterState(0)
}
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 ObjInstance).data as CounterState
ObjInt.of(st.count)
},
set = { _, instance, value ->
val st = (instance as ObjInstance).data as CounterState
st.count = (value as ObjInt).value
}
)
addFun("secret") { _, _, _ -> ObjInt.of(42) }
addFun("ping") { _, _, _ -> ObjInt.of(7) }
}
```
## Notes
- Visibility is respected by usual Lyng access rules; private extern members can be used only within class code.
- Use `init { scope -> ... }` for receiver-style init, or `initWithInstance { scope, instance -> ... }` for explicit instance access.
- `classData` and `instance.data` are Kotlin-only payloads and do not appear in Lyng reflection.
- Binding after the first instance of a class is created throws a `ScriptError`.

View File

@ -0,0 +1,42 @@
# Kotlin Bridge Reflection Facade (Handles)
This note documents the Kotlin-side reflection facade used by extern bindings to access Lyng values efficiently.
## Overview
- `ScopeFacade.resolver()` returns a `BridgeResolver` bound to the current call scope.
- Resolution returns stable handles (val/var/callable/member) that avoid repeated name lookup.
- Handles are safe across calls; they re-resolve automatically when the frame changes.
- Call-by-name is available via `BridgeCallByName` for quick cached calls.
## Example: locals + functions
```kotlin
val facade = scope.asFacade()
val resolver = facade.resolver()
val x = resolver.resolveVar("x")
val add = resolver.resolveCallable("add")
val a = x.get(facade) as ObjInt
x.set(facade, ObjInt.of(a.value + 1))
val res = add.call(facade, Arguments(ObjInt.of(2), ObjInt.of(3))) as ObjInt
```
## Example: member handles
```kotlin
val f = scope["f"]!!.value as ObjInstance
val count = resolver.resolveMemberVar(f, "count")
val bump = resolver.resolveMemberCallable(f, "bump")
count.set(facade, ObjInt.of(10))
bump.call(facade)
```
## Notes
- Visibility rules are enforced exactly as in Lyng.
- `resolveExtensionCallable` treats extensions as member callables using wrapper names.
- `selfAs(type)` creates a receiver view (this@Base) when resolving members on the current `this`.