Add support for closed classes and enhancements to the Kotlin reflection bridge
This commit is contained in:
parent
7f2f99524f
commit
c5b8871c3a
@ -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.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) },
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|||||||
59
lynglib/src/commonTest/kotlin/ClosedClassTest.kt
Normal file
59
lynglib/src/commonTest/kotlin/ClosedClassTest.kt
Normal 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()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user