Add support for closed classes and enhancements to the Kotlin reflection bridge

This commit is contained in:
Sergey Chernov 2026-02-16 19:08:32 +03:00
parent 7f2f99524f
commit c5b8871c3a
7 changed files with 327 additions and 36 deletions

View File

@ -182,6 +182,77 @@ instance.value = 42
println(instance.value) // -> 42 println(instance.value) // -> 42
``` ```
### 6.5) Preferred: bind Kotlin implementations to declared Lyng classes
For extensions and libraries, the **preferred** workflow is Lyng‑first: declare the class and its members in Lyng, then bind the Kotlin implementations using the bridge.
This keeps Lyng semantics (visibility, overrides, type checks) in Lyng, while Kotlin supplies the behavior.
```lyng
// Lyng side (in a module)
class Counter {
extern var value: Int
extern fun inc(by: Int): Int
}
```
Note: members must be marked `extern` so the compiler emits the ABI slots that Kotlin bindings attach to. This applies to functions and properties bound via `addFun` / `addVal` / `addVar`.
```kotlin
// Kotlin side (binding)
val moduleScope = Script.newScope() // or an existing module scope
moduleScope.eval("class Counter { extern var value: Int; extern fun inc(by: Int): Int }")
moduleScope.bind("Counter") {
addVar(
name = "value",
get = { _, self -> self.readField(this, "value").value },
set = { _, self, v -> self.writeField(this, "value", v) }
)
addFun("inc") { _, self, args ->
val by = args.requiredArg<ObjInt>(0).value
val current = self.readField(this, "value").value as ObjInt
val next = ObjInt(current.value + by)
self.writeField(this, "value", next)
next
}
}
```
Notes:
- Binding must happen **before** the first instance is created.
- Use [LyngClassBridge] to bind by name/module, or by an already resolved `ObjClass`.
- Use `ObjInstance.data` / `ObjClass.classData` to attach Kotlin‑side state when needed.
### 6.6) Preferred: Kotlin reflection bridge for call‑by‑name
For Kotlin code that needs dynamic access to Lyng variables, functions, or members, use the bridge resolver.
It provides explicit, cached handles and predictable lookup rules.
```kotlin
val scope = Script.newScope()
scope.eval("""
val x = 40
fun add(a, b) = a + b
class Box { var value = 1 }
""")
val resolver = scope.resolver()
// Read a top‑level value
val x = resolver.resolveVal("x").get(scope)
// Call a function by name (cached inside the resolver)
val sum = (resolver as BridgeCallByName).callByName(scope, "add", Arguments(ObjInt(1), ObjInt(2)))
// Member access
val box = scope.eval("Box()")
val valueHandle = resolver.resolveMemberVar(box, "value")
valueHandle.set(scope, ObjInt(10))
val value = valueHandle.get(scope)
```
### 7) Read variable values back in Kotlin ### 7) Read variable values back in Kotlin
The simplest approach: evaluate an expression that yields the value and convert it. The simplest approach: evaluate an expression that yields the value and convert it.

View File

@ -243,3 +243,11 @@ You can enable it in **Settings | Lyng Formatter | Enable Lyng autocompletion**.
### Kotlin API: Exception Handling ### Kotlin API: Exception Handling
The `Obj.getLyngExceptionMessageWithStackTrace()` extension method has been added to simplify retrieving detailed error information from Lyng exception objects in Kotlin. Additionally, `getLyngExceptionMessage()` and `raiseAsExecutionError()` now accept an optional `Scope`, making it easier to use them when a scope is not immediately available. The `Obj.getLyngExceptionMessageWithStackTrace()` extension method has been added to simplify retrieving detailed error information from Lyng exception objects in Kotlin. Additionally, `getLyngExceptionMessage()` and `raiseAsExecutionError()` now accept an optional `Scope`, making it easier to use them when a scope is not immediately available.
### Kotlin API: Bridge Reflection and Class Binding (Preferred Extensions)
Lyng now provides a public Kotlin reflection bridge and a Lyng‑first class binding workflow. This is the **preferred** way to write Kotlin extensions and library integrations:
- **Bridge resolver**: explicit handles for values, vars, and callables with predictable lookup rules.
- **Class bridge binding**: declare classes/members in Lyng (marked `extern`) and bind the implementations in Kotlin before the first instance is created.
See **Embedding Lyng** for full samples and usage details.

View File

@ -31,6 +31,7 @@ data class ClassDeclSpec(
val startPos: Pos, val startPos: Pos,
val isExtern: Boolean, val isExtern: Boolean,
val isAbstract: Boolean, val isAbstract: Boolean,
val isClosed: Boolean,
val isObject: Boolean, val isObject: Boolean,
val isAnonymous: Boolean, val isAnonymous: Boolean,
val baseSpecs: List<ClassDeclBaseSpec>, val baseSpecs: List<ClassDeclBaseSpec>,
@ -118,6 +119,7 @@ internal suspend fun executeClassDecl(
val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray()).also { val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray()).also {
it.isAbstract = spec.isAbstract it.isAbstract = spec.isAbstract
it.isClosed = spec.isClosed
it.instanceConstructor = constructorCode it.instanceConstructor = constructorCode
it.constructorMeta = spec.constructorArgs it.constructorMeta = spec.constructorArgs
for (i in parentClasses.indices) { for (i in parentClasses.indices) {

View File

@ -5331,8 +5331,8 @@ class Compiler(
val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody) val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody)
if (!isMember && isClosed) if (!isMember && isClosed && currentToken.value != "class")
throw ScriptError(currentToken.pos, "modifier closed is only allowed for class members") throw ScriptError(currentToken.pos, "modifier closed at top level is only allowed for classes")
if (!isMember && isOverride && currentToken.value != "fun" && currentToken.value != "fn") if (!isMember && isOverride && currentToken.value != "fun" && currentToken.value != "fn")
throw ScriptError(currentToken.pos, "modifier override outside class is only allowed for extension functions") throw ScriptError(currentToken.pos, "modifier override outside class is only allowed for extension functions")
@ -5345,12 +5345,12 @@ class Compiler(
"var" -> parseVarDeclaration(true, visibility, isAbstract, isClosed, isOverride, isStatic, isExtern) "var" -> parseVarDeclaration(true, visibility, isAbstract, isClosed, isOverride, isStatic, isExtern)
"fun", "fn" -> parseFunctionDeclaration(visibility, isAbstract, isClosed, isOverride, isExtern, isStatic) "fun", "fn" -> parseFunctionDeclaration(visibility, isAbstract, isClosed, isOverride, isExtern, isStatic)
"class" -> { "class" -> {
if (isStatic || isClosed || isOverride) if (isStatic || isOverride)
throw ScriptError( throw ScriptError(
currentToken.pos, currentToken.pos,
"unsupported modifiers for class: ${modifiers.joinToString(" ")}" "unsupported modifiers for class: ${modifiers.joinToString(" ")}"
) )
parseClassDeclaration(isAbstract, isExtern) parseClassDeclaration(isAbstract, isExtern, isClosed)
} }
"object" -> { "object" -> {
@ -5478,8 +5478,11 @@ class Compiler(
"type" -> { "type" -> {
pendingDeclStart = id.pos pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc() pendingDeclDoc = consumePendingDoc()
if (!looksLikeTypeAliasDeclaration()) return null if (looksLikeTypeAliasDeclaration()) {
parseTypeAliasDeclaration() parseTypeAliasDeclaration()
} else {
null
}
} }
"try" -> parseTryStatement() "try" -> parseTryStatement()
@ -6116,6 +6119,7 @@ class Compiler(
startPos = startPos, startPos = startPos,
isExtern = false, isExtern = false,
isAbstract = false, isAbstract = false,
isClosed = false,
isObject = true, isObject = true,
isAnonymous = nameToken == null, isAnonymous = nameToken == null,
baseSpecs = baseSpecs.map { ClassDeclBaseSpec(it.name, it.args) }, baseSpecs = baseSpecs.map { ClassDeclBaseSpec(it.name, it.args) },
@ -6127,7 +6131,7 @@ class Compiler(
return ClassDeclStatement(spec) return ClassDeclStatement(spec)
} }
private suspend fun parseClassDeclaration(isAbstract: Boolean = false, isExtern: Boolean = false): Statement { private suspend fun parseClassDeclaration(isAbstract: Boolean = false, isExtern: Boolean = false, isClosed: Boolean = false): Statement {
val nameToken = cc.requireToken(Token.Type.ID) val nameToken = cc.requireToken(Token.Type.ID)
val startPos = pendingDeclStart ?: nameToken.pos val startPos = pendingDeclStart ?: nameToken.pos
val doc = pendingDeclDoc ?: consumePendingDoc() val doc = pendingDeclDoc ?: consumePendingDoc()
@ -6474,6 +6478,7 @@ class Compiler(
startPos = startPos, startPos = startPos,
isExtern = isExtern, isExtern = isExtern,
isAbstract = isAbstract, isAbstract = isAbstract,
isClosed = isClosed,
isObject = false, isObject = false,
isAnonymous = false, isAnonymous = false,
baseSpecs = baseSpecs.map { ClassDeclBaseSpec(it.name, it.args) }, baseSpecs = baseSpecs.map { ClassDeclBaseSpec(it.name, it.args) },

View File

@ -1,15 +1,27 @@
/*
* 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.
*
*/
/* /*
* Kotlin bridge reflection facade: handle-based access for fast get/set/call. * Kotlin bridge reflection facade: handle-based access for fast get/set/call.
*/ */
package net.sergeych.lyng.bridge package net.sergeych.lyng.bridge
import net.sergeych.lyng.Arguments import net.sergeych.lyng.*
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.Obj
import net.sergeych.lyng.obj.ObjClass import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjIllegalAccessException import net.sergeych.lyng.obj.ObjIllegalAccessException
@ -20,54 +32,85 @@ import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.obj.ObjUnset import net.sergeych.lyng.obj.ObjUnset
import net.sergeych.lyng.obj.ObjVoid import net.sergeych.lyng.obj.ObjVoid
import net.sergeych.lyng.requireScope import net.sergeych.lyng.requireScope
import net.sergeych.lyng.ModuleScope
/** Where to resolve names from. */ /**
* Where a bridge resolver should search for names.
*
* Used by [LookupSpec] to control reflection scope for Kotlin-side tooling and bindings.
*/
enum class LookupTarget { enum class LookupTarget {
/** Resolve from the current frame only (locals/params declared in the active scope). */
CurrentFrame, CurrentFrame,
/** Resolve by walking the raw parent chain of frames (locals only, no member fallback). */
ParentChain, ParentChain,
/** Resolve against the module frame (top-level declarations in the module). */
ModuleFrame ModuleFrame
} }
/** Explicit receiver view (like this@Base). */ /**
* Explicit receiver view, similar to `this@Base` in Lyng.
*
* When provided, the resolver will treat `this` as the requested type
* for member resolution and visibility checks.
*/
data class ReceiverView( data class ReceiverView(
val type: ObjClass? = null, val type: ObjClass? = null,
val typeName: String? = null val typeName: String? = null
) )
/** Lookup rules for bridge resolution. */ /**
* Lookup rules for bridge resolution.
*
* @property targets where to resolve names from
* @property receiverView optional explicit receiver for member lookup (like `this@Base`)
*/
data class LookupSpec( data class LookupSpec(
val targets: Set<LookupTarget> = setOf(LookupTarget.CurrentFrame, LookupTarget.ModuleFrame), val targets: Set<LookupTarget> = setOf(LookupTarget.CurrentFrame, LookupTarget.ModuleFrame),
val receiverView: ReceiverView? = null val receiverView: ReceiverView? = null
) )
/** Base handle type. */ /**
* Base handle type returned by the Kotlin reflection bridge.
*
* Handles are inexpensive to keep and cache; they resolve lazily and
* may internally cache slots/records once a frame is known.
*/
sealed interface BridgeHandle { sealed interface BridgeHandle {
/** Name of the underlying symbol (as written in Lyng). */
val name: String val name: String
} }
/** Read-only value handle. */ /** Read-only value handle resolved in a [ScopeFacade]. */
interface ValHandle : BridgeHandle { interface ValHandle : BridgeHandle {
/** Read the current value. */
suspend fun get(scope: ScopeFacade): Obj suspend fun get(scope: ScopeFacade): Obj
} }
/** Read/write value handle. */ /** Read/write value handle resolved in a [ScopeFacade]. */
interface VarHandle : ValHandle { interface VarHandle : ValHandle {
/** Assign a new value. */
suspend fun set(scope: ScopeFacade, value: Obj) suspend fun set(scope: ScopeFacade, value: Obj)
} }
/** Callable handle (function/closure/method). */ /** Callable handle (function/closure/method). */
interface CallableHandle : BridgeHandle { interface CallableHandle : BridgeHandle {
/**
* Call the target with optional [args].
*
* @param newThisObj overrides receiver for member calls (defaults to current `this`/record receiver).
*/
suspend fun call(scope: ScopeFacade, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Obj suspend fun call(scope: ScopeFacade, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Obj
} }
/** Member handle resolved against an instance or receiver view. */ /** Member handle resolved against an instance or receiver view. */
interface MemberHandle : BridgeHandle { interface MemberHandle : BridgeHandle {
/** Declaring class resolved for the last call/get/set (if known). */
val declaringClass: ObjClass? val declaringClass: ObjClass?
/** Explicit receiver view used for resolution (if any). */
val receiverView: ReceiverView? val receiverView: ReceiverView?
} }
/** Member field/property. */ /** Member field/property (read-only). */
interface MemberValHandle : MemberHandle, ValHandle interface MemberValHandle : MemberHandle, ValHandle
/** Member var/property with write access. */ /** Member var/property with write access. */
@ -76,41 +119,64 @@ interface MemberVarHandle : MemberHandle, VarHandle
/** Member callable (method or extension). */ /** Member callable (method or extension). */
interface MemberCallableHandle : MemberHandle, CallableHandle interface MemberCallableHandle : MemberHandle, CallableHandle
/** Direct record handle (debug/inspection). */ /**
* Direct record handle (debug/inspection).
*
* Exposes raw [ObjRecord] access and should be used only in tooling.
*/
interface RecordHandle : BridgeHandle { interface RecordHandle : BridgeHandle {
/** Resolve and return the raw [ObjRecord]. */
fun record(): ObjRecord fun record(): ObjRecord
} }
/** Bridge resolver API (entry point for Kotlin bindings). */ /**
* Bridge resolver API (entry point for Kotlin reflection and bindings).
*
* Obtain via [ScopeFacade.resolver] and reuse for multiple lookups.
* Resolver methods return handles that can be cached and reused across calls.
*/
interface BridgeResolver { interface BridgeResolver {
/** Source position used for error reporting. */
val pos: Pos val pos: Pos
/** Treat `this` as [type] for member lookup (like `this@Type`). */
fun selfAs(type: ObjClass): BridgeResolver fun selfAs(type: ObjClass): BridgeResolver
/** Treat `this` as [typeName] for member lookup (like `this@Type`). */
fun selfAs(typeName: String): BridgeResolver fun selfAs(typeName: String): BridgeResolver
/** Resolve a read-only value by name using [lookup]. */
fun resolveVal(name: String, lookup: LookupSpec = LookupSpec()): ValHandle fun resolveVal(name: String, lookup: LookupSpec = LookupSpec()): ValHandle
/** Resolve a mutable value by name using [lookup]. */
fun resolveVar(name: String, lookup: LookupSpec = LookupSpec()): VarHandle fun resolveVar(name: String, lookup: LookupSpec = LookupSpec()): VarHandle
/** Resolve a callable by name using [lookup]. */
fun resolveCallable(name: String, lookup: LookupSpec = LookupSpec()): CallableHandle fun resolveCallable(name: String, lookup: LookupSpec = LookupSpec()): CallableHandle
/** Resolve a member value on [receiver]. */
fun resolveMemberVal( fun resolveMemberVal(
receiver: Obj, receiver: Obj,
name: String, name: String,
lookup: LookupSpec = LookupSpec() lookup: LookupSpec = LookupSpec()
): MemberValHandle ): MemberValHandle
/** Resolve a mutable member on [receiver]. */
fun resolveMemberVar( fun resolveMemberVar(
receiver: Obj, receiver: Obj,
name: String, name: String,
lookup: LookupSpec = LookupSpec() lookup: LookupSpec = LookupSpec()
): MemberVarHandle ): MemberVarHandle
/** Resolve a member callable on [receiver]. */
fun resolveMemberCallable( fun resolveMemberCallable(
receiver: Obj, receiver: Obj,
name: String, name: String,
lookup: LookupSpec = LookupSpec() lookup: LookupSpec = LookupSpec()
): MemberCallableHandle ): MemberCallableHandle
/** Extension function treated as a member for reflection. */ /**
* Resolve an extension function treated as a member for reflection.
*
* This uses the extension wrapper name (same rules as Lyng compiler).
*/
fun resolveExtensionCallable( fun resolveExtensionCallable(
receiverClass: ObjClass, receiverClass: ObjClass,
name: String, name: String,
@ -119,14 +185,20 @@ interface BridgeResolver {
/** Debug: resolve locals by name (optional, for tooling). */ /** Debug: resolve locals by name (optional, for tooling). */
fun resolveLocalVal(name: String): ValHandle fun resolveLocalVal(name: String): ValHandle
/** Debug: resolve mutable locals by name (optional, for tooling). */
fun resolveLocalVar(name: String): VarHandle fun resolveLocalVar(name: String): VarHandle
/** Debug: access raw record handles if needed. */ /** Debug: access raw record handles if needed. */
fun resolveRecord(name: String, lookup: LookupSpec = LookupSpec()): RecordHandle fun resolveRecord(name: String, lookup: LookupSpec = LookupSpec()): RecordHandle
} }
/** Convenience: call by name with implicit caching in resolver implementation. */ /**
* Convenience: call by name with implicit caching in resolver implementation.
*
* Implemented by the default resolver; useful for lightweight call-by-name flows.
*/
interface BridgeCallByName { interface BridgeCallByName {
/** Resolve and call [name] with [args] using [lookup]. */
suspend fun callByName( suspend fun callByName(
scope: ScopeFacade, scope: ScopeFacade,
name: String, name: String,
@ -135,12 +207,21 @@ interface BridgeCallByName {
): Obj ): Obj
} }
/** Optional typed wrappers (sugar). */ /**
* Optional typed wrapper (sugar) around [ValHandle].
*
* Performs a runtime cast to [T] and raises a class cast error on mismatch.
*/
interface TypedHandle<T : Obj> : ValHandle { interface TypedHandle<T : Obj> : ValHandle {
/** Read value and cast it to [T]. */
suspend fun getTyped(scope: ScopeFacade): T suspend fun getTyped(scope: ScopeFacade): T
} }
/** Factory for bridge resolver. */ /**
* Factory for bridge resolver.
*
* Prefer this over ad-hoc lookups when writing Kotlin extensions or tooling.
*/
fun ScopeFacade.resolver(): BridgeResolver = BridgeResolverImpl(this) fun ScopeFacade.resolver(): BridgeResolver = BridgeResolverImpl(this)
private class BridgeResolverImpl( private class BridgeResolverImpl(

View File

@ -1,13 +1,28 @@
/*
* 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.
*
*/
/* /*
* Kotlin bridge bindings for Lyng classes (Lyng-first workflow). * Kotlin bridge bindings for Lyng classes (Lyng-first workflow).
*/ */
package net.sergeych.lyng.bridge package net.sergeych.lyng.bridge
import net.sergeych.lyng.Arguments import net.sergeych.lyng.*
import net.sergeych.lyng.Pos import net.sergeych.lyng.bytecode.BytecodeStatement
import net.sergeych.lyng.ScopeFacade
import net.sergeych.lyng.Script
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjExternCallable import net.sergeych.lyng.obj.ObjExternCallable
@ -16,24 +31,41 @@ import net.sergeych.lyng.obj.ObjProperty
import net.sergeych.lyng.obj.ObjRecord import net.sergeych.lyng.obj.ObjRecord
import net.sergeych.lyng.obj.ObjVoid import net.sergeych.lyng.obj.ObjVoid
import net.sergeych.lyng.pacman.ImportManager 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.requiredArg
import net.sergeych.lyng.InstanceFieldInitStatement
import net.sergeych.lyng.Statement
import net.sergeych.lyng.bytecode.BytecodeStatement
/**
* Per-instance bridge context passed to init hooks.
*
* Exposes the underlying [instance] and a mutable [data] slot for Kotlin-side state.
*/
interface BridgeInstanceContext { interface BridgeInstanceContext {
/** The Lyng instance being initialized. */
val instance: Obj val instance: Obj
/** Arbitrary Kotlin-side data attached to the instance. */
var data: Any? var data: Any?
} }
/**
* Binder DSL for attaching Kotlin implementations to a declared Lyng class.
*
* Use [LyngClassBridge.bind] to obtain a binder and register implementations.
* Bindings must happen before the first instance of the class is created.
*
* Important: members you bind here must be declared as `extern` in Lyng so the
* compiler emits the ABI slots that Kotlin bindings attach to.
*/
interface ClassBridgeBinder { interface ClassBridgeBinder {
/** Arbitrary Kotlin-side data attached to the class. */
var classData: Any? var classData: Any?
/** Register an initialization hook that runs for each instance. */
fun init(block: suspend BridgeInstanceContext.(ScopeFacade) -> Unit) fun init(block: suspend BridgeInstanceContext.(ScopeFacade) -> Unit)
/** Register an initialization hook with direct access to the instance. */
fun initWithInstance(block: suspend (ScopeFacade, Obj) -> Unit) fun initWithInstance(block: suspend (ScopeFacade, Obj) -> Unit)
/** Bind a Lyng function/member to a Kotlin implementation (requires `extern` in Lyng). */
fun addFun(name: String, impl: suspend (ScopeFacade, Obj, Arguments) -> Obj) fun addFun(name: String, impl: suspend (ScopeFacade, Obj, Arguments) -> Obj)
/** Bind a read-only member (val/property getter) declared as `extern`. */
fun addVal(name: String, impl: suspend (ScopeFacade, Obj) -> Obj) fun addVal(name: String, impl: suspend (ScopeFacade, Obj) -> Obj)
/** Bind a mutable member (var/property getter/setter) declared as `extern`. */
fun addVar( fun addVar(
name: String, name: String,
get: suspend (ScopeFacade, Obj) -> Obj, get: suspend (ScopeFacade, Obj) -> Obj,
@ -41,7 +73,20 @@ interface ClassBridgeBinder {
) )
} }
/**
* Entry point for Kotlin bindings to declared Lyng classes.
*
* The workflow is Lyng-first: declare the class and its members in Lyng,
* then bind the implementations from Kotlin. Bound members must be marked
* `extern` so the compiler emits the ABI slots for Kotlin to attach to.
*/
object LyngClassBridge { object LyngClassBridge {
/**
* Resolve a Lyng class by [className] 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( suspend fun bind(
className: String, className: String,
module: String? = null, module: String? = null,
@ -52,6 +97,9 @@ object LyngClassBridge {
return bind(cls, block) return bind(cls, block)
} }
/**
* Resolve a Lyng class within an existing [moduleScope] and bind Kotlin implementations.
*/
suspend fun bind( suspend fun bind(
moduleScope: ModuleScope, moduleScope: ModuleScope,
className: String, className: String,
@ -61,6 +109,11 @@ object LyngClassBridge {
return bind(cls, block) return bind(cls, block)
} }
/**
* Bind Kotlin implementations to an already resolved [clazz].
*
* This must run before the first instance is created.
*/
fun bind(clazz: ObjClass, block: ClassBridgeBinder.() -> Unit): ObjClass { fun bind(clazz: ObjClass, block: ClassBridgeBinder.() -> Unit): ObjClass {
val binder = ClassBridgeBinderImpl(clazz) val binder = ClassBridgeBinderImpl(clazz)
binder.block() binder.block()
@ -69,10 +122,22 @@ object LyngClassBridge {
} }
} }
/**
* Sugar for [LyngClassBridge.bind] on a module scope.
*
* Bound members must be declared as `extern` in Lyng.
*/
suspend fun ModuleScope.bind(
className: String,
block: ClassBridgeBinder.() -> Unit
): ObjClass = LyngClassBridge.bind(this, className, block)
/** Kotlin-side data slot attached to a Lyng instance. */
var ObjInstance.data: Any? var ObjInstance.data: Any?
get() = kotlinInstanceData get() = kotlinInstanceData
set(value) { kotlinInstanceData = value } set(value) { kotlinInstanceData = value }
/** Kotlin-side data slot attached to a Lyng class. */
var ObjClass.classData: Any? var ObjClass.classData: Any?
get() = kotlinClassData get() = kotlinClassData
set(value) { kotlinClassData = value } set(value) { kotlinClassData = value }

View File

@ -0,0 +1,59 @@
/*
* 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.
*
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError
import kotlin.test.Test
import kotlin.test.assertFailsWith
class ClosedClassTest {
@Test
fun testClosedClass() = runTest {
val scope = Script.newScope()
scope.eval("""
closed class MyClosedClass {
fun foo() = 42
}
""".trimIndent())
assertFailsWith<ScriptError> {
scope.eval("""
class SubClass : MyClosedClass()
""".trimIndent())
}
}
@Test
fun testStdlibClosedClasses() = runTest {
val scope = Script.newScope()
assertFailsWith<ScriptError> {
scope.eval("class MyInt : Int()")
}
assertFailsWith<ScriptError> {
scope.eval("class MyReal : Real()")
}
assertFailsWith<ScriptError> {
scope.eval("class MyString : String()")
}
assertFailsWith<ScriptError> {
scope.eval("class MyBool : Bool()")
}
}
}