Enforce bytecode-only wrappers and add extern bridge opcode

This commit is contained in:
Sergey Chernov 2026-02-13 04:46:07 +03:00
parent 05cba5b653
commit 489dae6604
14 changed files with 232 additions and 33 deletions

View File

@ -74,7 +74,7 @@ internal suspend fun executeClassDecl(
newClass.classScope = classScope
classScope.addConst("object", newClass)
spec.bodyInit?.execute(classScope)
spec.bodyInit?.let { requireBytecodeBody(scope, it, "object body init").execute(classScope) }
val instance = newClass.callOn(scope.createChildScope(Arguments.EMPTY))
if (spec.declaredName != null) {
@ -148,16 +148,29 @@ internal suspend fun executeClassDecl(
}
classScope.currentClassCtx = newClass
newClass.classScope = classScope
spec.bodyInit?.execute(classScope)
spec.bodyInit?.let { requireBytecodeBody(scope, it, "class body init").execute(classScope) }
if (spec.initScope.isNotEmpty()) {
for (s in spec.initScope) {
s.execute(classScope)
requireBytecodeBody(scope, s, "class init").execute(classScope)
}
}
newClass.checkAbstractSatisfaction(spec.startPos)
return newClass
}
private suspend fun requireBytecodeBody(
scope: Scope,
stmt: Statement,
label: String
): net.sergeych.lyng.bytecode.BytecodeStatement {
val bytecode = when (stmt) {
is net.sergeych.lyng.bytecode.BytecodeStatement -> stmt
is BytecodeBodyProvider -> stmt.bytecodeBody()
else -> null
}
return bytecode ?: scope.raiseIllegalState("$label requires bytecode statement")
}
class ClassDeclStatement(
val spec: ClassDeclSpec,
) : Statement() {

View File

@ -176,6 +176,7 @@ class Compiler(
private val encodedPayloadTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
private val encodedPayloadTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
private val objectDeclNames: MutableSet<String> = mutableSetOf()
private val externCallableNames: MutableSet<String> = mutableSetOf()
private fun seedSlotPlanFromScope(scope: Scope, includeParents: Boolean = false) {
val plan = moduleSlotPlan() ?: return
@ -1853,6 +1854,7 @@ class Compiler(
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
externCallableNames = externCallableNames,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
)
}
@ -1876,6 +1878,7 @@ class Compiler(
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
externCallableNames = externCallableNames,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
)
}
@ -1920,6 +1923,7 @@ class Compiler(
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
externCallableNames = externCallableNames,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
)
}
@ -6690,6 +6694,12 @@ class Compiler(
if (extensionWrapperName != null) {
declareLocalName(extensionWrapperName, isMutable = false)
}
if (actualExtern && declKind != SymbolKind.MEMBER) {
externCallableNames.add(name)
}
if (actualExtern && extensionWrapperName != null) {
externCallableNames.add(extensionWrapperName)
}
val declSlotPlan = if (declKind != SymbolKind.MEMBER) slotPlanStack.lastOrNull() else null
val declSlotIndex = declSlotPlan?.slots?.get(name)?.index
val declScopeId = declSlotPlan?.id

View File

@ -18,6 +18,7 @@ package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjExternCallable
import net.sergeych.lyng.obj.ObjExtensionMethodCallable
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjRecord
@ -66,21 +67,22 @@ internal suspend fun executeFunctionDecl(
if (spec.actualExtern && spec.extTypeName == null && !spec.parentIsClassBody) {
val existing = scope.get(spec.name)
if (existing != null) {
val value = (existing.value as? ObjExternCallable) ?: ObjExternCallable.wrap(existing.value)
scope.addItem(
spec.name,
false,
existing.value,
value,
spec.visibility,
callSignature = existing.callSignature
)
return existing.value
return value
}
}
if (spec.isDelegated) {
val delegateExpr = spec.delegateExpression ?: scope.raiseError("delegated function missing delegate")
val accessType = ObjString("Callable")
val initValue = delegateExpr.execute(scope)
val initValue = requireBytecodeBody(scope, delegateExpr, "delegated function").execute(scope)
val finalDelegate = try {
initValue.invokeInstanceMethod(scope, "bind", Arguments(ObjString(spec.name), accessType, scope.thisObj))
} catch (e: Exception) {
@ -143,7 +145,7 @@ internal suspend fun executeFunctionDecl(
)
val initStmt = spec.delegateInitStatement
?: scope.raiseIllegalState("missing delegated init statement for ${spec.name}")
cls.instanceInitializers += initStmt
cls.instanceInitializers += requireBytecodeBody(scope, initStmt, "delegated function init")
} else {
scope.addItem(
spec.name,
@ -225,6 +227,19 @@ internal suspend fun executeFunctionDecl(
return annotatedFnBody
}
private suspend fun requireBytecodeBody(
scope: Scope,
stmt: Statement,
label: String
): net.sergeych.lyng.bytecode.BytecodeStatement {
val bytecode = when (stmt) {
is net.sergeych.lyng.bytecode.BytecodeStatement -> stmt
is BytecodeBodyProvider -> stmt.bytecodeBody()
else -> null
}
return bytecode ?: scope.raiseIllegalState("$label requires bytecode statement")
}
class FunctionDeclStatement(
val spec: FunctionDeclSpec,
) : Statement() {

View File

@ -28,10 +28,19 @@ class InlineBlockStatement(
override suspend fun execute(scope: Scope): Obj {
var last: Obj = ObjVoid
for (stmt in statements) {
last = stmt.execute(scope)
last = requireBytecodeBody(scope, stmt, "inline block").execute(scope)
}
return last
}
fun statements(): List<Statement> = statements
private suspend fun requireBytecodeBody(scope: Scope, stmt: Statement, label: String): net.sergeych.lyng.bytecode.BytecodeStatement {
val bytecode = when (stmt) {
is net.sergeych.lyng.bytecode.BytecodeStatement -> stmt
is BytecodeBodyProvider -> stmt.bytecodeBody()
else -> null
}
return bytecode ?: scope.raiseIllegalState("$label requires bytecode statement")
}
}

View File

@ -20,7 +20,6 @@ import net.sergeych.lyng.bytecode.CmdFrame
import net.sergeych.lyng.bytecode.CmdVm
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjNull
import net.sergeych.lyng.obj.ObjRecord
class PropertyAccessorStatement(
val body: Statement,
@ -29,32 +28,33 @@ class PropertyAccessorStatement(
) : Statement() {
override suspend fun execute(scope: Scope): Obj {
if (argName != null) {
val value = scope.args.list.firstOrNull() ?: ObjNull
val prev = scope.skipScopeCreation
scope.skipScopeCreation = true
return try {
if (body is net.sergeych.lyng.bytecode.BytecodeStatement) {
val fn = body.bytecodeFunction()
val binder: suspend (CmdFrame, Arguments) -> Unit = { frame, arguments ->
val slotPlan = fn.localSlotPlanByName()
val slotIndex = slotPlan[argName]
val argValue = arguments.list.firstOrNull() ?: ObjNull
if (slotIndex != null) {
frame.frame.setObj(slotIndex, argValue)
} else if (scope.getLocalRecordDirect(argName) == null) {
scope.addItem(argName, true, argValue, recordType = ObjRecord.Type.Argument)
}
}
scope.pos = pos
CmdVm().execute(fn, scope, scope.args, binder)
} else {
scope.addItem(argName, true, value, recordType = ObjRecord.Type.Argument)
body.execute(scope)
val bytecodeStmt = requireBytecodeBody(scope, body, "property accessor")
val fn = bytecodeStmt.bytecodeFunction()
val binder: suspend (CmdFrame, Arguments) -> Unit = { frame, arguments ->
val slotPlan = fn.localSlotPlanByName()
val slotIndex = slotPlan[argName]
?: scope.raiseIllegalState("property accessor argument $argName missing from slot plan")
val argValue = arguments.list.firstOrNull() ?: ObjNull
frame.frame.setObj(slotIndex, argValue)
}
scope.pos = pos
CmdVm().execute(fn, scope, scope.args, binder)
} finally {
scope.skipScopeCreation = prev
}
}
return body.execute(scope)
return requireBytecodeBody(scope, body, "property accessor").execute(scope)
}
private suspend fun requireBytecodeBody(scope: Scope, stmt: Statement, label: String): net.sergeych.lyng.bytecode.BytecodeStatement {
val bytecode = when (stmt) {
is net.sergeych.lyng.bytecode.BytecodeStatement -> stmt
is BytecodeBodyProvider -> stmt.bytecodeBody()
else -> null
}
return bytecode ?: scope.raiseIllegalState("$label requires bytecode statement")
}
}

View File

@ -78,7 +78,6 @@ open class Scope(
thisObj = primary
val extrasSnapshot = when {
extras.isEmpty() -> emptyList()
extras === thisVariants -> extras.toList()
extras is MutableList<*> -> synchronized(extras) { extras.toList() }
else -> extras.toList()
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* 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.
*/
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjRecord
/**
* Limited facade for Kotlin bridge callables.
* Exposes only the minimal API needed to read/write vars and invoke methods.
*/
interface ScopeFacade {
val args: Arguments
var pos: Pos
var thisObj: Obj
operator fun get(name: String): ObjRecord?
suspend fun resolve(rec: ObjRecord, name: String): Obj
suspend fun assign(rec: ObjRecord, name: String, newValue: Obj)
fun raiseError(message: String): Nothing
fun raiseSymbolNotFound(name: String): Nothing
fun raiseIllegalState(message: String = "Illegal argument error"): Nothing
}
internal class ScopeBridge(private val scope: Scope) : ScopeFacade {
override val args: Arguments
get() = scope.args
override var pos: Pos
get() = scope.pos
set(value) { scope.pos = value }
override var thisObj: Obj
get() = scope.thisObj
set(value) { scope.thisObj = value }
override fun get(name: String): ObjRecord? = scope[name]
override suspend fun resolve(rec: ObjRecord, name: String): Obj = scope.resolve(rec, name)
override suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) = scope.assign(rec, name, newValue)
override fun raiseError(message: String): Nothing = scope.raiseError(message)
override fun raiseSymbolNotFound(name: String): Nothing = scope.raiseSymbolNotFound(name)
override fun raiseIllegalState(message: String): Nothing = scope.raiseIllegalState(message)
}

View File

@ -35,6 +35,7 @@ class BytecodeCompiler(
private val enumEntriesByName: Map<String, List<String>> = emptyMap(),
private val callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
private val callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
private val externCallableNames: Set<String> = emptySet(),
private val lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
) {
private var builder = CmdBuilder()
@ -3688,6 +3689,7 @@ class BytecodeCompiler(
private fun compileCall(ref: CallRef): CompiledValue? {
val callPos = callSitePos()
val localTarget = ref.target as? LocalVarRef
val isExternCall = localTarget != null && externCallableNames.contains(localTarget.name)
if (localTarget != null) {
val direct = resolveDirectNameSlot(localTarget.name)
if (direct == null) {
@ -3711,7 +3713,13 @@ class BytecodeCompiler(
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, encodedCount, dst)
builder.emit(
if (isExternCall) Opcode.CALL_BRIDGE_SLOT else Opcode.CALL_SLOT,
callee.slot,
args.base,
encodedCount,
dst
)
if (initClass != null) {
slotObjClass[dst] = initClass
}
@ -3730,7 +3738,13 @@ class BytecodeCompiler(
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, encodedCount, dst)
builder.emit(
if (isExternCall) Opcode.CALL_BRIDGE_SLOT else Opcode.CALL_SLOT,
callee.slot,
args.base,
encodedCount,
dst
)
if (initClass != null) {
slotObjClass[dst] = initClass
}

View File

@ -53,6 +53,7 @@ class BytecodeStatement private constructor(
enumEntriesByName: Map<String, List<String>> = emptyMap(),
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
externCallableNames: Set<String> = emptySet(),
lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
): Statement {
if (statement is BytecodeStatement) return statement
@ -80,6 +81,7 @@ class BytecodeStatement private constructor(
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
externCallableNames = externCallableNames,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
)
val compiled = compiler.compileStatement(nameHint, statement)

View File

@ -197,7 +197,7 @@ class CmdBuilder {
listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_MEMBER_SLOT ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_SLOT ->
Opcode.CALL_SLOT, Opcode.CALL_BRIDGE_SLOT ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_DYNAMIC_MEMBER ->
listOf(OperandKind.SLOT, OperandKind.CONST, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
@ -435,6 +435,7 @@ class CmdBuilder {
Opcode.ASSIGN_DESTRUCTURE -> CmdAssignDestructure(operands[0], operands[1])
Opcode.CALL_MEMBER_SLOT -> CmdCallMemberSlot(operands[0], operands[1], operands[2], operands[3], operands[4])
Opcode.CALL_SLOT -> CmdCallSlot(operands[0], operands[1], operands[2], operands[3])
Opcode.CALL_BRIDGE_SLOT -> CmdCallBridgeSlot(operands[0], operands[1], operands[2], operands[3])
Opcode.CALL_DYNAMIC_MEMBER -> CmdCallDynamicMember(operands[0], operands[1], operands[2], operands[3], operands[4])
Opcode.GET_INDEX -> CmdGetIndex(operands[0], operands[1], operands[2])
Opcode.SET_INDEX -> CmdSetIndex(operands[0], operands[1], operands[2])

View File

@ -229,6 +229,7 @@ object CmdDisassembler {
is CmdAssignDestructure -> Opcode.ASSIGN_DESTRUCTURE to intArrayOf(cmd.constId, cmd.slot)
is CmdCallMemberSlot -> Opcode.CALL_MEMBER_SLOT to intArrayOf(cmd.recvSlot, cmd.methodId, cmd.argBase, cmd.argCount, cmd.dst)
is CmdCallSlot -> Opcode.CALL_SLOT to intArrayOf(cmd.calleeSlot, cmd.argBase, cmd.argCount, cmd.dst)
is CmdCallBridgeSlot -> Opcode.CALL_BRIDGE_SLOT to intArrayOf(cmd.calleeSlot, cmd.argBase, cmd.argCount, cmd.dst)
is CmdCallDynamicMember -> Opcode.CALL_DYNAMIC_MEMBER to intArrayOf(cmd.recvSlot, cmd.nameId, cmd.argBase, cmd.argCount, cmd.dst)
is CmdGetIndex -> Opcode.GET_INDEX to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.dst)
is CmdSetIndex -> Opcode.SET_INDEX to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.valueSlot)
@ -330,7 +331,7 @@ object CmdDisassembler {
listOf(OperandKind.SLOT, OperandKind.IP)
Opcode.CALL_DIRECT ->
listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_SLOT ->
Opcode.CALL_SLOT, Opcode.CALL_BRIDGE_SLOT ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_MEMBER_SLOT ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)

View File

@ -2023,6 +2023,40 @@ class CmdCallSlot(
}
}
class CmdCallBridgeSlot(
internal val calleeSlot: Int,
internal val argBase: Int,
internal val argCount: Int,
internal val dst: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val callee = frame.slotToObj(calleeSlot)
if (callee === ObjUnset) {
val name = if (calleeSlot < frame.fn.scopeSlotCount) {
frame.fn.scopeSlotNames[calleeSlot]
} else {
val localIndex = calleeSlot - frame.fn.scopeSlotCount
frame.fn.localSlotNames.getOrNull(localIndex)
}
val message = name?.let { "property '$it' is unset (not initialized)" }
?: "property is unset (not initialized) in ${frame.fn.name} at slot $calleeSlot"
frame.ensureScope().raiseUnset(message)
}
if (callee !is net.sergeych.lyng.obj.ObjExternCallable) {
frame.ensureScope().raiseIllegalState("CALL_BRIDGE_SLOT expects extern callable")
}
val args = frame.buildArguments(argBase, argCount)
val result = if (PerfFlags.SCOPE_POOL) {
frame.ensureScope().withChildFrame(args) { child -> callee.callOn(child) }
} else {
val scope = frame.ensureScope()
callee.callOn(scope.createChildScope(scope.pos, args = args))
}
frame.storeObjResult(dst, result)
return
}
}
class CmdListLiteral(
internal val planId: Int,
internal val baseSlot: Int,

View File

@ -136,6 +136,7 @@ enum class Opcode(val code: Int) {
ASSIGN_DESTRUCTURE(0x91),
CALL_MEMBER_SLOT(0x92),
CALL_SLOT(0x93),
CALL_BRIDGE_SLOT(0x94),
GET_INDEX(0xA2),
SET_INDEX(0xA3),

View File

@ -0,0 +1,47 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* 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.
*/
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeBridge
import net.sergeych.lyng.ScopeFacade
import net.sergeych.lyng.Statement
class ObjExternCallable private constructor(
private val target: Obj?,
private val fn: (suspend ScopeFacade.() -> Obj)?
) : Obj() {
override val objClass: ObjClass
get() = Statement.type
override suspend fun callOn(scope: Scope): Obj {
val facade = ScopeBridge(scope)
return when {
fn != null -> facade.fn()
target != null -> target.callOn(scope)
else -> ObjVoid
}
}
override fun toString(): String = "ExternCallable@${hashCode()}"
companion object {
fun wrap(target: Obj): ObjExternCallable = ObjExternCallable(target, null)
fun fromBridge(fn: suspend ScopeFacade.() -> Obj): ObjExternCallable = ObjExternCallable(null, fn)
}
}