diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt index 7364109..cd2f973 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt @@ -73,6 +73,9 @@ internal class ScopeBridge(internal val scope: Scope) : ScopeFacade { override fun trace(text: String) = scope.trace(text) } +/** Public factory for bridge facades. */ +fun Scope.asFacade(): ScopeFacade = ScopeBridge(this) + inline fun ScopeFacade.requiredArg(index: Int): T { if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}") return (args.list[index].byValueCopy() as? T) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/BridgeResolver.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/BridgeResolver.kt new file mode 100644 index 0000000..19bc46c --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/BridgeResolver.kt @@ -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 = 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 : 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 = 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 ?: "") + } + + 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 { + 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( + private val inner: ValHandle, + private val clazzName: String +) : TypedHandle { + 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") + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/ClassBridge.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/ClassBridge.kt new file mode 100644 index 0000000..93279f8 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bridge/ClassBridge.kt @@ -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 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(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 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}") +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index c38f918..409cab7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -288,6 +288,14 @@ open class ObjClass( private var methodSlotLayoutVersion: Int = -1 private var methodSlotMap: Map = emptyMap() 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 Unit>? = null + + internal var instanceTemplateBuilt: Boolean = false private fun ensureFieldSlots(): Map { if (fieldSlotLayoutVersion == layoutVersion) return fieldSlotMap @@ -480,12 +488,20 @@ open class ObjClass( val existingId = rec.methodId if (existingId != null) { methodIdMap[name] = existingId + if (existingId >= nextMethodId) { + nextMethodId = existingId + 1 + } return existingId } val id = methodIdMap.getOrPut(name) { nextMethodId++ } + if (id >= nextMethodId) { + nextMethodId = id + 1 + } return id } + internal fun ensureMethodIdForBridge(name: String, rec: ObjRecord): Int = assignMethodId(name, rec) + private fun ensureMethodIdSeeded() { if (methodIdSeeded) return var maxId = -1 @@ -515,6 +531,17 @@ open class ObjClass( 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 suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1 @@ -552,6 +579,7 @@ open class ObjClass( } } } + instanceTemplateBuilt = true res } @@ -634,6 +662,14 @@ open class ObjClass( args: Arguments?, runConstructors: Boolean ) { + bridgeInitHooks?.let { hooks -> + if (hooks.isNotEmpty()) { + val facade = instance.instanceScope.asFacade() + for (hook in hooks) { + hook(facade, instance) + } + } + } val visited = hashSetOf() initClassInternal(instance, visited, this, args, isRoot = true, runConstructors = runConstructors) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt index 2ed7f97..ec5ab43 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -29,6 +29,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { internal lateinit var instanceScope: Scope internal var fieldSlots: Array = emptyArray() internal var methodSlots: Array = emptyArray() + internal var kotlinInstanceData: Any? = null internal fun initFieldSlots(size: Int) { fieldSlots = arrayOfNulls(size) diff --git a/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt b/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt new file mode 100644 index 0000000..ae0eaff --- /dev/null +++ b/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt @@ -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) + } +} diff --git a/lynglib/src/commonTest/kotlin/BridgeResolverTest.kt b/lynglib/src/commonTest/kotlin/BridgeResolverTest.kt new file mode 100644 index 0000000..dde5d7f --- /dev/null +++ b/lynglib/src/commonTest/kotlin/BridgeResolverTest.kt @@ -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) + } +} diff --git a/notes/bridge_facade_draft.kt b/notes/bridge_facade_draft.kt new file mode 100644 index 0000000..edbdf2e --- /dev/null +++ b/notes/bridge_facade_draft.kt @@ -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 = 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 : ValHandle { + suspend fun getTyped(scope: ScopeFacade): T +} + +/** Debug view of frame stack for tooling. */ +interface FrameStackView { + fun frames(): List +} + +interface FrameView { + val pos: Pos + val thisObj: Obj + fun locals(): Map +} + +/** 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? +} diff --git a/notes/kotlin_bridge_binding.md b/notes/kotlin_bridge_binding.md new file mode 100644 index 0000000..95307b0 --- /dev/null +++ b/notes/kotlin_bridge_binding.md @@ -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`. diff --git a/notes/kotlin_bridge_reflection.md b/notes/kotlin_bridge_reflection.md new file mode 100644 index 0000000..ed6d440 --- /dev/null +++ b/notes/kotlin_bridge_reflection.md @@ -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`.