diff --git a/docs/BytecodeSpec.md b/docs/BytecodeSpec.md index 9eccaa8..86e136e 100644 --- a/docs/BytecodeSpec.md +++ b/docs/BytecodeSpec.md @@ -55,6 +55,7 @@ Behavior: Other calls: - CALL_VIRTUAL recvSlot, methodId, argBase, argCount, dst - CALL_FALLBACK stmtId, argBase, argCount, dst +- CALL_SLOT calleeSlot, argBase, argCount, dst ## 4) Binary Encoding Layout @@ -89,6 +90,7 @@ Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass. - MOVE_INT S -> S - MOVE_REAL S -> S - MOVE_BOOL S -> S +- BOX_OBJ S -> S - CONST_OBJ K -> S - CONST_INT K -> S - CONST_REAL K -> S @@ -188,6 +190,7 @@ Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass. - CALL_DIRECT F, S, C, S - CALL_VIRTUAL S, M, S, C, S - CALL_FALLBACK T, S, C, S +- CALL_SLOT S, S, C, S ### Object access (optional, later) - GET_FIELD S, M -> S diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 4bc2cda..84deea0 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.2.1-SNAPSHOT" +version = "1.3.0-SNAPSHOT" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt index ca5c8d6..b966375 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt @@ -130,7 +130,7 @@ class BytecodeBuilder { private fun operandKinds(op: Opcode): List { return when (op) { Opcode.NOP, Opcode.RET_VOID -> emptyList() - Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, + Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ, Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL, Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT -> listOf(OperandKind.SLOT, OperandKind.SLOT) @@ -162,6 +162,8 @@ class BytecodeBuilder { listOf(OperandKind.SLOT, OperandKind.IP) Opcode.CALL_DIRECT, Opcode.CALL_FALLBACK -> listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) + Opcode.CALL_SLOT -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) Opcode.CALL_VIRTUAL -> listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) Opcode.GET_FIELD -> diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index a8f693d..4efd5f2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -18,6 +18,7 @@ package net.sergeych.lyng.bytecode import net.sergeych.lyng.ExpressionStatement import net.sergeych.lyng.IfStatement +import net.sergeych.lyng.ParsedArgument import net.sergeych.lyng.Pos import net.sergeych.lyng.Statement import net.sergeych.lyng.ToBoolStatement @@ -70,6 +71,8 @@ class BytecodeCompiler( is BinaryOpRef -> compileBinary(ref) is UnaryOpRef -> compileUnary(ref) is AssignRef -> compileAssign(ref) + is CallRef -> compileCall(ref) + is MethodCallRef -> compileMethodCall(ref) else -> null } } @@ -587,6 +590,64 @@ class BytecodeCompiler( return CompiledValue(slot, value.type) } + private data class CallArgs(val base: Int, val count: Int) + + private fun compileCall(ref: CallRef): CompiledValue? { + if (ref.isOptionalInvoke) return null + if (!argsEligible(ref.args, ref.tailBlock)) return null + val callee = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null + val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null + val dst = allocSlot() + builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, args.count, dst) + return CompiledValue(dst, SlotType.UNKNOWN) + } + + private fun compileMethodCall(ref: MethodCallRef): CompiledValue? { + if (ref.isOptional) return null + if (!argsEligible(ref.args, ref.tailBlock)) return null + val receiver = compileRefWithFallback(ref.receiver, null, Pos.builtIn) ?: return null + val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null + val methodId = builder.addConst(BytecodeConst.StringVal(ref.name)) + if (methodId > 0xFFFF) return null + val dst = allocSlot() + builder.emit(Opcode.CALL_VIRTUAL, receiver.slot, methodId, args.base, args.count, dst) + return CompiledValue(dst, SlotType.UNKNOWN) + } + + private fun argsEligible(args: List, tailBlock: Boolean): Boolean { + if (tailBlock) return false + for (arg in args) { + if (arg.isSplat || arg.name != null) return false + if (arg.value !is ExpressionStatement) return false + } + return true + } + + private fun compileCallArgs(args: List, tailBlock: Boolean): CallArgs? { + if (tailBlock) return null + for (arg in args) { + if (arg.isSplat || arg.name != null) return null + } + if (args.isEmpty()) return CallArgs(base = 0, count = 0) + val argSlots = IntArray(args.size) { allocSlot() } + for ((index, arg) in args.withIndex()) { + val stmt = arg.value + val compiled = if (stmt is ExpressionStatement) { + compileRefWithFallback(stmt.ref, null, stmt.pos) + } else { + null + } ?: return null + val dst = argSlots[index] + if (compiled.slot != dst) { + builder.emit(Opcode.BOX_OBJ, compiled.slot, dst) + } else if (compiled.type != SlotType.OBJ) { + builder.emit(Opcode.BOX_OBJ, compiled.slot, dst) + } + updateSlotType(dst, SlotType.OBJ) + } + return CallArgs(base = argSlots[0], count = argSlots.size) + } + private fun compileIf(name: String, stmt: IfStatement): BytecodeFunction? { val conditionStmt = stmt.condition as? ExpressionStatement ?: return null val condValue = compileRefWithFallback(conditionStmt.ref, SlotType.BOOL, stmt.pos) ?: return null @@ -636,11 +697,13 @@ class BytecodeCompiler( } private fun compileRefWithFallback(ref: ObjRef, forceType: SlotType?, pos: Pos): CompiledValue? { - val compiled = compileRef(ref) - if (compiled != null && (forceType == null || compiled.type == forceType || compiled.type == SlotType.UNKNOWN)) { - return if (forceType != null && compiled.type == SlotType.UNKNOWN) { - CompiledValue(compiled.slot, forceType) - } else compiled + var compiled = compileRef(ref) + if (compiled != null) { + if (forceType == null) return compiled + if (compiled.type == forceType) return compiled + if (compiled.type == SlotType.UNKNOWN) { + compiled = null + } } val slot = allocSlot() val stmt = if (forceType == SlotType.BOOL) { @@ -734,9 +797,26 @@ class BytecodeCompiler( } collectScopeSlotsRef(assignValue(ref)) } + is CallRef -> { + collectScopeSlotsRef(ref.target) + collectScopeSlotsArgs(ref.args) + } + is MethodCallRef -> { + collectScopeSlotsRef(ref.receiver) + collectScopeSlotsArgs(ref.args) + } else -> {} } } + private fun collectScopeSlotsArgs(args: List) { + for (arg in args) { + val stmt = arg.value + if (stmt is ExpressionStatement) { + collectScopeSlotsRef(stmt.ref) + } + } + } + private data class ScopeSlotKey(val depth: Int, val slot: Int) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt index f9fd62e..995c280 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt @@ -83,7 +83,7 @@ object BytecodeDisassembler { private fun operandKinds(op: Opcode): List { return when (op) { Opcode.NOP, Opcode.RET_VOID -> emptyList() - Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, + Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ, Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL, Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT -> listOf(OperandKind.SLOT, OperandKind.SLOT) @@ -115,6 +115,8 @@ object BytecodeDisassembler { listOf(OperandKind.SLOT, OperandKind.IP) Opcode.CALL_DIRECT, Opcode.CALL_FALLBACK -> listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) + Opcode.CALL_SLOT -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) Opcode.CALL_VIRTUAL -> listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) Opcode.GET_FIELD -> diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFunction.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFunction.kt index 3f0da78..4109b47 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFunction.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFunction.kt @@ -30,6 +30,8 @@ data class BytecodeFunction( val fallbackStatements: List, val code: ByteArray, ) { + val methodCallSites: MutableMap = mutableMapOf() + init { require(slotWidth == 1 || slotWidth == 2 || slotWidth == 4) { "slotWidth must be 1,2,4" } require(ipWidth == 2 || ipWidth == 4) { "ipWidth must be 2 or 4" } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt index 27d4a51..a046a95 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt @@ -16,6 +16,8 @@ package net.sergeych.lyng.bytecode +import net.sergeych.lyng.Arguments +import net.sergeych.lyng.PerfFlags import net.sergeych.lyng.Scope import net.sergeych.lyng.obj.* @@ -34,6 +36,7 @@ class BytecodeVm { var ip = 0 val code = fn.code while (ip < code.size) { + val startIp = ip val op = decoder.readOpcode(code, ip) ip += 1 when (op) { @@ -119,6 +122,13 @@ class BytecodeVm { ip += fn.slotWidth setObj(fn, frame, scope, dst, getObj(fn, frame, scope, src)) } + Opcode.BOX_OBJ -> { + val src = decoder.readSlot(code, ip) + ip += fn.slotWidth + val dst = decoder.readSlot(code, ip) + ip += fn.slotWidth + setObj(fn, frame, scope, dst, slotToObj(fn, frame, scope, src)) + } Opcode.INT_TO_REAL -> { val src = decoder.readSlot(code, ip) ip += fn.slotWidth @@ -711,6 +721,53 @@ class BytecodeVm { ip = target } } + Opcode.CALL_SLOT -> { + val calleeSlot = decoder.readSlot(code, ip) + ip += fn.slotWidth + val argBase = decoder.readSlot(code, ip) + ip += fn.slotWidth + val argCount = decoder.readConstId(code, ip, 2) + ip += 2 + val dst = decoder.readSlot(code, ip) + ip += fn.slotWidth + val callee = slotToObj(fn, frame, scope, calleeSlot) + val args = buildArguments(fn, frame, scope, argBase, argCount) + val result = if (PerfFlags.SCOPE_POOL) { + scope.withChildFrame(args) { child -> callee.callOn(child) } + } else { + callee.callOn(scope.createChildScope(scope.pos, args = args)) + } + when (result) { + is ObjInt -> setInt(fn, frame, scope, dst, result.value) + is ObjReal -> setReal(fn, frame, scope, dst, result.value) + is ObjBool -> setBool(fn, frame, scope, dst, result.value) + else -> setObj(fn, frame, scope, dst, result) + } + } + Opcode.CALL_VIRTUAL -> { + val recvSlot = decoder.readSlot(code, ip) + ip += fn.slotWidth + val methodId = decoder.readConstId(code, ip, 2) + ip += 2 + val argBase = decoder.readSlot(code, ip) + ip += fn.slotWidth + val argCount = decoder.readConstId(code, ip, 2) + ip += 2 + val dst = decoder.readSlot(code, ip) + ip += fn.slotWidth + val receiver = slotToObj(fn, frame, scope, recvSlot) + val nameConst = fn.constants.getOrNull(methodId) as? BytecodeConst.StringVal + ?: error("CALL_VIRTUAL expects StringVal at $methodId") + val args = buildArguments(fn, frame, scope, argBase, argCount) + val site = fn.methodCallSites.getOrPut(startIp) { MethodCallSite(nameConst.value) } + val result = site.invoke(scope, receiver, args) + when (result) { + is ObjInt -> setInt(fn, frame, scope, dst, result.value) + is ObjReal -> setReal(fn, frame, scope, dst, result.value) + is ObjBool -> setBool(fn, frame, scope, dst, result.value) + else -> setObj(fn, frame, scope, dst, result) + } + } Opcode.EVAL_FALLBACK -> { val id = decoder.readConstId(code, ip, 2) ip += 2 @@ -751,6 +808,21 @@ class BytecodeVm { } } + private fun buildArguments( + fn: BytecodeFunction, + frame: BytecodeFrame, + scope: Scope, + argBase: Int, + argCount: Int, + ): Arguments { + if (argCount == 0) return Arguments.EMPTY + val list = ArrayList(argCount) + for (i in 0 until argCount) { + list.add(slotToObj(fn, frame, scope, argBase + i)) + } + return Arguments(list) + } + private fun getObj(fn: BytecodeFunction, frame: BytecodeFrame, scope: Scope, slot: Int): Obj { return if (slot < fn.scopeSlotCount) { resolveScope(scope, fn.scopeSlotDepths[slot]).getSlotRecord(fn.scopeSlotIndices[slot]).value diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/MethodCallSite.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/MethodCallSite.kt new file mode 100644 index 0000000..8775069 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/MethodCallSite.kt @@ -0,0 +1,241 @@ +/* + * 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.bytecode + +import net.sergeych.lyng.Arguments +import net.sergeych.lyng.ExecutionError +import net.sergeych.lyng.PerfFlags +import net.sergeych.lyng.PerfStats +import net.sergeych.lyng.Scope +import net.sergeych.lyng.Visibility +import net.sergeych.lyng.canAccessMember +import net.sergeych.lyng.obj.Obj +import net.sergeych.lyng.obj.ObjClass +import net.sergeych.lyng.obj.ObjIllegalAccessException +import net.sergeych.lyng.obj.ObjInstance +import net.sergeych.lyng.obj.ObjProperty +import net.sergeych.lyng.obj.ObjRecord + +class MethodCallSite(private val name: String) { + private var mKey1: Long = 0L; private var mVer1: Int = -1 + private var mInvoker1: (suspend (Obj, Scope, Arguments) -> Obj)? = null + private var mKey2: Long = 0L; private var mVer2: Int = -1 + private var mInvoker2: (suspend (Obj, Scope, Arguments) -> Obj)? = null + private var mKey3: Long = 0L; private var mVer3: Int = -1 + private var mInvoker3: (suspend (Obj, Scope, Arguments) -> Obj)? = null + private var mKey4: Long = 0L; private var mVer4: Int = -1 + private var mInvoker4: (suspend (Obj, Scope, Arguments) -> Obj)? = null + + private var mAccesses: Int = 0; private var mMisses: Int = 0; private var mPromotedTo4: Boolean = false + private var mFreezeWindowsLeft: Int = 0 + private var mWindowAccesses: Int = 0 + private var mWindowMisses: Int = 0 + + private inline fun size4MethodsEnabled(): Boolean = + PerfFlags.METHOD_PIC_SIZE_4 || + ((PerfFlags.PIC_ADAPTIVE_2_TO_4 || PerfFlags.PIC_ADAPTIVE_METHODS_ONLY) && mPromotedTo4 && mFreezeWindowsLeft == 0) + + private fun noteMethodHit() { + if (!(PerfFlags.PIC_ADAPTIVE_2_TO_4 || PerfFlags.PIC_ADAPTIVE_METHODS_ONLY)) return + val a = (mAccesses + 1).coerceAtMost(1_000_000) + mAccesses = a + if (PerfFlags.PIC_ADAPTIVE_HEURISTIC) { + mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000) + if (mWindowAccesses >= 256) endHeuristicWindow() + } + } + + private fun noteMethodMiss() { + if (!(PerfFlags.PIC_ADAPTIVE_2_TO_4 || PerfFlags.PIC_ADAPTIVE_METHODS_ONLY)) return + val a = (mAccesses + 1).coerceAtMost(1_000_000) + mAccesses = a + mMisses = (mMisses + 1).coerceAtMost(1_000_000) + if (!mPromotedTo4 && mFreezeWindowsLeft == 0 && a >= 256) { + if (mMisses * 100 / a > 20) mPromotedTo4 = true + mAccesses = 0; mMisses = 0 + } + if (PerfFlags.PIC_ADAPTIVE_HEURISTIC) { + mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000) + mWindowMisses = (mWindowMisses + 1).coerceAtMost(1_000_000) + if (mWindowAccesses >= 256) endHeuristicWindow() + } + } + + private fun endHeuristicWindow() { + val accesses = mWindowAccesses + val misses = mWindowMisses + mWindowAccesses = 0 + mWindowMisses = 0 + if (mFreezeWindowsLeft > 0) { + mFreezeWindowsLeft = (mFreezeWindowsLeft - 1).coerceAtLeast(0) + return + } + if (mPromotedTo4 && accesses >= 256) { + val rate = misses * 100 / accesses + if (rate >= 25) { + mPromotedTo4 = false + mFreezeWindowsLeft = 4 + } + } + } + + suspend fun invoke(scope: Scope, base: Obj, callArgs: Arguments): Obj { + if (PerfFlags.METHOD_PIC) { + val (key, ver) = when (base) { + is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion + is ObjClass -> base.classId to base.layoutVersion + else -> 0L to -1 + } + if (key != 0L) { + mInvoker1?.let { inv -> + if (key == mKey1 && ver == mVer1) { + if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.methodPicHit++ + noteMethodHit() + return inv(base, scope, callArgs) + } + } + mInvoker2?.let { inv -> + if (key == mKey2 && ver == mVer2) { + if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.methodPicHit++ + noteMethodHit() + val tK = mKey2; val tV = mVer2; val tI = mInvoker2 + mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 + mKey1 = tK; mVer1 = tV; mInvoker1 = tI + return inv(base, scope, callArgs) + } + } + if (size4MethodsEnabled()) mInvoker3?.let { inv -> + if (key == mKey3 && ver == mVer3) { + if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.methodPicHit++ + noteMethodHit() + val tK = mKey3; val tV = mVer3; val tI = mInvoker3 + mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 + mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 + mKey1 = tK; mVer1 = tV; mInvoker1 = tI + return inv(base, scope, callArgs) + } + } + if (size4MethodsEnabled()) mInvoker4?.let { inv -> + if (key == mKey4 && ver == mVer4) { + if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.methodPicHit++ + noteMethodHit() + val tK = mKey4; val tV = mVer4; val tI = mInvoker4 + mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 + mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 + mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 + mKey1 = tK; mVer1 = tV; mInvoker1 = tI + return inv(base, scope, callArgs) + } + } + if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.methodPicMiss++ + noteMethodMiss() + val result = try { + base.invokeInstanceMethod(scope, name, callArgs) + } catch (e: ExecutionError) { + mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 + mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 + mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 + mKey1 = key; mVer1 = ver; mInvoker1 = { _, sc, _ -> + sc.raiseError(e.message ?: "method not found: $name") + } + throw e + } + if (size4MethodsEnabled()) { + mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 + mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 + } + mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 + when (base) { + is ObjInstance -> { + val cls0 = base.objClass + val keyInScope = cls0.publicMemberResolution[name] + val methodSlot = if (keyInScope != null) cls0.methodSlotForKey(keyInScope) else null + val fastRec = if (methodSlot != null) { + val idx = methodSlot.slot + if (idx >= 0 && idx < base.methodSlots.size) base.methodSlots[idx] else null + } else if (keyInScope != null) { + base.methodRecordForKey(keyInScope) ?: base.instanceScope.objects[keyInScope] + } else null + val resolved = if (fastRec != null) null else cls0.resolveInstanceMember(name) + val targetRec = when { + fastRec != null && fastRec.type == ObjRecord.Type.Fun -> fastRec + resolved != null && resolved.record.type == ObjRecord.Type.Fun && !resolved.record.isAbstract -> resolved.record + else -> null + } + if (targetRec != null) { + val visibility = targetRec.visibility + val decl = targetRec.declaringClass ?: (resolved?.declaringClass ?: cls0) + if (methodSlot != null && targetRec.type == ObjRecord.Type.Fun) { + val slotIndex = methodSlot.slot + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> + val inst = obj as ObjInstance + if (inst.objClass === cls0) { + val rec = if (slotIndex >= 0 && slotIndex < inst.methodSlots.size) inst.methodSlots[slotIndex] else null + if (rec != null && rec.type == ObjRecord.Type.Fun && !rec.isAbstract) { + if (!visibility.isPublic && !canAccessMember(visibility, decl, sc.currentClassCtx, name)) { + sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name")) + } + rec.value.invoke(inst.instanceScope, inst, a, decl) + } else { + obj.invokeInstanceMethod(sc, name, a) + } + } else { + obj.invokeInstanceMethod(sc, name, a) + } + } + } else { + val callable = targetRec.value + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> + val inst = obj as ObjInstance + if (!visibility.isPublic && !canAccessMember(visibility, decl, sc.currentClassCtx, name)) { + sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name")) + } + callable.invoke(inst.instanceScope, inst, a) + } + } + } else { + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> + obj.invokeInstanceMethod(sc, name, a) + } + } + } + is ObjClass -> { + val clsScope = base.classScope + val rec = clsScope?.get(name) + if (rec != null) { + val callable = rec.value + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> + callable.invoke(sc, obj, a) + } + } else { + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> + obj.invokeInstanceMethod(sc, name, a) + } + } + } + else -> { + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> + obj.invokeInstanceMethod(sc, name, a) + } + } + } + return result + } + } + return base.invokeInstanceMethod(scope, name, callArgs) + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt index c22aff9..d3de9cb 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -27,6 +27,7 @@ enum class Opcode(val code: Int) { CONST_REAL(0x07), CONST_BOOL(0x08), CONST_NULL(0x09), + BOX_OBJ(0x0A), INT_TO_REAL(0x10), REAL_TO_INT(0x11), @@ -110,6 +111,7 @@ enum class Opcode(val code: Int) { CALL_DIRECT(0x90), CALL_VIRTUAL(0x91), CALL_FALLBACK(0x92), + CALL_SLOT(0x93), GET_FIELD(0xA0), SET_FIELD(0xA1), diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt index 4f5e9ad..aa8d918 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -166,8 +166,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { } del = del ?: scope.raiseError("Internal error: delegated property $name has no delegate") val res = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))) - obj.value = res - return obj + return obj.copy(value = res, type = ObjRecord.Type.Other) } // Map member template to instance storage if applicable diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index d06a19b..d301a34 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -1155,6 +1155,17 @@ class FieldRef( else -> 0L to -1 // no caching for primitives/dynamics without stable shape } + private suspend fun resolveValue(scope: Scope, base: Obj, rec: ObjRecord): Obj { + if (rec.type == ObjRecord.Type.Delegated || rec.value is ObjProperty || rec.type == ObjRecord.Type.Property) { + val receiver = rec.receiver ?: base + return receiver.resolveRecord(scope, rec, name, rec.declaringClass).value + } + if (rec.receiver != null && rec.declaringClass != null) { + return rec.receiver!!.resolveRecord(scope, rec, name, rec.declaringClass).value + } + return rec.value + } + override suspend fun evalValue(scope: Scope): Obj { // Mirror get(), but return raw Obj to avoid transient ObjRecord on R-value paths val fieldPic = PerfFlags.FIELD_PIC @@ -1172,14 +1183,14 @@ class FieldRef( if (key != 0L) { rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { if (picCounters) PerfStats.fieldPicHit++ - return g(base, scope).value + return resolveValue(scope, base, g(base, scope)) } } rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { if (picCounters) PerfStats.fieldPicHit++ val tK = rKey2; val tV = rVer2; val tG = rGetter2 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey1 = tK; rVer1 = tV; rGetter1 = tG - return g(base, scope).value + return resolveValue(scope, base, g(base, scope)) } } if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { if (picCounters) PerfStats.fieldPicHit++ @@ -1187,7 +1198,7 @@ class FieldRef( rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey1 = tK; rVer1 = tV; rGetter1 = tG - return g(base, scope).value + return resolveValue(scope, base, g(base, scope)) } } if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { if (picCounters) PerfStats.fieldPicHit++ @@ -1196,16 +1207,17 @@ class FieldRef( rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey1 = tK; rVer1 = tV; rGetter1 = tG - return g(base, scope).value + return resolveValue(scope, base, g(base, scope)) } } if (picCounters) PerfStats.fieldPicMiss++ val rec = base.readField(scope, name) // install primary generic getter for this shape rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) } - return rec.value + return resolveValue(scope, base, rec) } } - return base.readField(scope, name).value + val rec = base.readField(scope, name) + return resolveValue(scope, base, rec) } } @@ -1567,10 +1579,10 @@ class StatementRef(internal val statement: Statement) : ObjRef { * Direct function call reference: f(args) and optional f?(args). */ class CallRef( - private val target: ObjRef, - private val args: List, - private val tailBlock: Boolean, - private val isOptionalInvoke: Boolean, + internal val target: ObjRef, + internal val args: List, + internal val tailBlock: Boolean, + internal val isOptionalInvoke: Boolean, ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { val usePool = PerfFlags.SCOPE_POOL @@ -1592,11 +1604,11 @@ class CallRef( * Instance method call reference: obj.method(args) and optional obj?.method(args). */ class MethodCallRef( - private val receiver: ObjRef, - private val name: String, - private val args: List, - private val tailBlock: Boolean, - private val isOptional: Boolean, + internal val receiver: ObjRef, + internal val name: String, + internal val args: List, + internal val tailBlock: Boolean, + internal val isOptional: Boolean, ) : ObjRef { // 4-entry PIC for method invocations (guarded by PerfFlags.METHOD_PIC) private var mKey1: Long = 0L; private var mVer1: Int = -1; private var mInvoker1: (suspend (Obj, Scope, Arguments) -> Obj)? = null diff --git a/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt b/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt index 759ec28..5536106 100644 --- a/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt +++ b/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt @@ -16,7 +16,9 @@ import net.sergeych.lyng.ExpressionStatement import net.sergeych.lyng.IfStatement +import net.sergeych.lyng.Pos import net.sergeych.lyng.Scope +import net.sergeych.lyng.Statement import net.sergeych.lyng.bytecode.BytecodeBuilder import net.sergeych.lyng.bytecode.BytecodeCompiler import net.sergeych.lyng.bytecode.BytecodeConst @@ -38,6 +40,7 @@ import net.sergeych.lyng.obj.ObjVoid import net.sergeych.lyng.obj.toBool import net.sergeych.lyng.obj.toDouble import net.sergeych.lyng.obj.toInt +import net.sergeych.lyng.obj.toLong import kotlin.test.Test import kotlin.test.assertEquals @@ -151,6 +154,28 @@ class BytecodeVmTest { assertEquals(5.75, result.toDouble()) } + @Test + fun callSlotInvokesCallable() = kotlinx.coroutines.test.runTest { + val callable = object : Statement() { + override val pos: Pos = Pos.builtIn + override suspend fun execute(scope: Scope) = ObjInt.of( + scope.args[0].toLong() + scope.args[1].toLong() + ) + } + val builder = BytecodeBuilder() + val fnId = builder.addConst(BytecodeConst.ObjRef(callable)) + val arg0 = builder.addConst(BytecodeConst.IntVal(2L)) + val arg1 = builder.addConst(BytecodeConst.IntVal(3L)) + builder.emit(Opcode.CONST_OBJ, fnId, 0) + builder.emit(Opcode.CONST_INT, arg0, 1) + builder.emit(Opcode.CONST_INT, arg1, 2) + builder.emit(Opcode.CALL_SLOT, 0, 1, 2, 3) + builder.emit(Opcode.RET, 3) + val fn = builder.build("callSlot", localCount = 4) + val result = BytecodeVm().execute(fn, Scope(), emptyList()) + assertEquals(5, result.toInt()) + } + @Test fun mixedIntRealComparisonUsesBytecodeOps() = kotlinx.coroutines.test.runTest { val ltExpr = ExpressionStatement( diff --git a/notes/bytecode_callsite_fix.md b/notes/bytecode_callsite_fix.md new file mode 100644 index 0000000..149f6a7 --- /dev/null +++ b/notes/bytecode_callsite_fix.md @@ -0,0 +1,15 @@ +# Bytecode call-site PIC + fallback gating + +Changes +- Added method call PIC path in bytecode VM with new CALL_SLOT/CALL_VIRTUAL opcodes. +- Fixed FieldRef property/delegate resolution to avoid bypassing ObjRecord delegation. +- Prevent delegated ObjRecord mutation by returning a resolved copy. +- Restricted bytecode call compilation to args that are ExpressionStatement (no splat/named/tail-block), fallback otherwise. + +Rationale +- Fixes JVM test regressions and avoids premature evaluation of Statement args. +- Keeps delegated/property semantics identical to interpreter. + +Tests +- ./gradlew :lynglib:jvmTest +- ./gradlew :lynglib:allTests -x :lynglib:jvmTest