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

This commit is contained in:
sergeych committed 2026-02-16 19:08:32 +03:00
1 parent 7f2f99524f
commit c5b8871c3a
7 files changed
+327 -36

No files matched your search

@@ -31,6 +31,7 @@ data class ClassDeclSpec(
val startPos: Pos,
val isExtern: Boolean,
val isAbstract: Boolean,
val isClosed: Boolean,
val isObject: Boolean,
val isAnonymous: Boolean,
val baseSpecs: List<ClassDeclBaseSpec>,
@@ -118,6 +119,7 @@ internal suspend fun executeClassDecl(
val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray()).also {
it.isAbstract = spec.isAbstract
it.isClosed = spec.isClosed
it.instanceConstructor = constructorCode
it.constructorMeta = spec.constructorArgs
for (i in parentClasses.indices) {
@@ -5331,8 +5331,8 @@ class Compiler(
val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody)
if (!isMember && isClosed)
throw ScriptError(currentToken.pos, "modifier closed is only allowed for class members")
if (!isMember && isClosed && currentToken.value != "class")
throw ScriptError(currentToken.pos, "modifier closed at top level is only allowed for classes")
if (!isMember && isOverride && currentToken.value != "fun" && currentToken.value != "fn")
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)
"fun", "fn" -> parseFunctionDeclaration(visibility, isAbstract, isClosed, isOverride, isExtern, isStatic)
"class" -> {
if (isStatic || isClosed || isOverride)
if (isStatic || isOverride)
throw ScriptError(
currentToken.pos,
"unsupported modifiers for class: ${modifiers.joinToString(" ")}"
)
parseClassDeclaration(isAbstract, isExtern)
parseClassDeclaration(isAbstract, isExtern, isClosed)
}
"object" -> {
@@ -5478,8 +5478,11 @@ class Compiler(
"type" -> {
pendingDeclStart = id.pos
pendingDeclDoc = consumePendingDoc()
if (!looksLikeTypeAliasDeclaration()) return null
parseTypeAliasDeclaration()
if (looksLikeTypeAliasDeclaration()) {
parseTypeAliasDeclaration()
} else {
null
}
}
"try" -> parseTryStatement()
@@ -6116,6 +6119,7 @@ class Compiler(
startPos = startPos,
isExtern = false,
isAbstract = false,
isClosed = false,
isObject = true,
isAnonymous = nameToken == null,
baseSpecs = baseSpecs.map { ClassDeclBaseSpec(it.name, it.args) },
@@ -6127,7 +6131,7 @@ class Compiler(
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 startPos = pendingDeclStart ?: nameToken.pos
val doc = pendingDeclDoc ?: consumePendingDoc()
@@ -6474,6 +6478,7 @@ class Compiler(
startPos = startPos,
isExtern = isExtern,
isAbstract = isAbstract,
isClosed = isClosed,
isObject = false,
isAnonymous = false,
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.
*/
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.*
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
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.ObjVoid
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 {
/** Resolve from the current frame only (locals/params declared in the active scope). */
CurrentFrame,
/** Resolve by walking the raw parent chain of frames (locals only, no member fallback). */
ParentChain,
/** Resolve against the module frame (top-level declarations in the module). */
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(
val type: ObjClass? = 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(
val targets: Set<LookupTarget> = setOf(LookupTarget.CurrentFrame, LookupTarget.ModuleFrame),
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 {
/** Name of the underlying symbol (as written in Lyng). */
val name: String
}
/** Read-only value handle. */
/** Read-only value handle resolved in a [ScopeFacade]. */
interface ValHandle : BridgeHandle {
/** Read the current value. */
suspend fun get(scope: ScopeFacade): Obj
}
/** Read/write value handle. */
/** Read/write value handle resolved in a [ScopeFacade]. */
interface VarHandle : ValHandle {
/** Assign a new value. */
suspend fun set(scope: ScopeFacade, value: Obj)
}
/** Callable handle (function/closure/method). */
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
}
/** Member handle resolved against an instance or receiver view. */
interface MemberHandle : BridgeHandle {
/** Declaring class resolved for the last call/get/set (if known). */
val declaringClass: ObjClass?
/** Explicit receiver view used for resolution (if any). */
val receiverView: ReceiverView?
}
/** Member field/property. */
/** Member field/property (read-only). */
interface MemberValHandle : MemberHandle, ValHandle
/** Member var/property with write access. */
@@ -76,41 +119,64 @@ interface MemberVarHandle : MemberHandle, VarHandle
/** Member callable (method or extension). */
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 {
/** Resolve and return the raw [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 {
/** Source position used for error reporting. */
val pos: Pos
/** Treat `this` as [type] for member lookup (like `this@Type`). */
fun selfAs(type: ObjClass): BridgeResolver
/** Treat `this` as [typeName] for member lookup (like `this@Type`). */
fun selfAs(typeName: String): BridgeResolver
/** Resolve a read-only value by name using [lookup]. */
fun resolveVal(name: String, lookup: LookupSpec = LookupSpec()): ValHandle
/** Resolve a mutable value by name using [lookup]. */
fun resolveVar(name: String, lookup: LookupSpec = LookupSpec()): VarHandle
/** Resolve a callable by name using [lookup]. */
fun resolveCallable(name: String, lookup: LookupSpec = LookupSpec()): CallableHandle
/** Resolve a member value on [receiver]. */
fun resolveMemberVal(
receiver: Obj,
name: String,
lookup: LookupSpec = LookupSpec()
): MemberValHandle
/** Resolve a mutable member on [receiver]. */
fun resolveMemberVar(
receiver: Obj,
name: String,
lookup: LookupSpec = LookupSpec()
): MemberVarHandle
/** Resolve a member callable on [receiver]. */
fun resolveMemberCallable(
receiver: Obj,
name: String,
lookup: LookupSpec = LookupSpec()
): 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(
receiverClass: ObjClass,
name: String,
@@ -119,14 +185,20 @@ interface BridgeResolver {
/** Debug: resolve locals by name (optional, for tooling). */
fun resolveLocalVal(name: String): ValHandle
/** Debug: resolve mutable locals by name (optional, for tooling). */
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. */
/**
* Convenience: call by name with implicit caching in resolver implementation.
*
* Implemented by the default resolver; useful for lightweight call-by-name flows.
*/
interface BridgeCallByName {
/** Resolve and call [name] with [args] using [lookup]. */
suspend fun callByName(
scope: ScopeFacade,
name: String,
@@ -135,12 +207,21 @@ interface BridgeCallByName {
): 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 {
/** Read value and cast it to [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)
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).
*/
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.*
import net.sergeych.lyng.bytecode.BytecodeStatement
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
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.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
/**
* Per-instance bridge context passed to init hooks.
*
* Exposes the underlying [instance] and a mutable [data] slot for Kotlin-side state.
*/
interface BridgeInstanceContext {
/** The Lyng instance being initialized. */
val instance: Obj
/** Arbitrary Kotlin-side data attached to the instance. */
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 {
/** Arbitrary Kotlin-side data attached to the class. */
var classData: Any?
/** Register an initialization hook that runs for each instance. */
fun init(block: suspend BridgeInstanceContext.(ScopeFacade) -> Unit)
/** Register an initialization hook with direct access to the instance. */
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)
/** Bind a read-only member (val/property getter) declared as `extern`. */
fun addVal(name: String, impl: suspend (ScopeFacade, Obj) -> Obj)
/** Bind a mutable member (var/property getter/setter) declared as `extern`. */
fun addVar(
name: String,
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 {
/**
* 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(
className: String,
module: String? = null,
@@ -52,6 +97,9 @@ object LyngClassBridge {
return bind(cls, block)
}
/**
* Resolve a Lyng class within an existing [moduleScope] and bind Kotlin implementations.
*/
suspend fun bind(
moduleScope: ModuleScope,
className: String,
@@ -61,6 +109,11 @@ object LyngClassBridge {
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 {
val binder = ClassBridgeBinderImpl(clazz)
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?
get() = kotlinInstanceData
set(value) { kotlinInstanceData = value }
/** Kotlin-side data slot attached to a Lyng class. */
var ObjClass.classData: Any?
get() = kotlinClassData
set(value) { kotlinClassData = value }
@@ -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()")
}
}
}