Fix bytecode call-site semantics

This commit is contained in:
Sergey Chernov 2026-01-26 04:09:49 +03:00
parent 059e366787
commit 72901d9d4c
13 changed files with 480 additions and 25 deletions

View File

@ -55,6 +55,7 @@ Behavior:
Other calls: Other calls:
- CALL_VIRTUAL recvSlot, methodId, argBase, argCount, dst - CALL_VIRTUAL recvSlot, methodId, argBase, argCount, dst
- CALL_FALLBACK stmtId, argBase, argCount, dst - CALL_FALLBACK stmtId, argBase, argCount, dst
- CALL_SLOT calleeSlot, argBase, argCount, dst
## 4) Binary Encoding Layout ## 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_INT S -> S
- MOVE_REAL S -> S - MOVE_REAL S -> S
- MOVE_BOOL S -> S - MOVE_BOOL S -> S
- BOX_OBJ S -> S
- CONST_OBJ K -> S - CONST_OBJ K -> S
- CONST_INT K -> S - CONST_INT K -> S
- CONST_REAL 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_DIRECT F, S, C, S
- CALL_VIRTUAL S, M, S, C, S - CALL_VIRTUAL S, M, S, C, S
- CALL_FALLBACK T, S, C, S - CALL_FALLBACK T, S, C, S
- CALL_SLOT S, S, C, S
### Object access (optional, later) ### Object access (optional, later)
- GET_FIELD S, M -> S - GET_FIELD S, M -> S

View File

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" 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 // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below

View File

@ -130,7 +130,7 @@ class BytecodeBuilder {
private fun operandKinds(op: Opcode): List<OperandKind> { private fun operandKinds(op: Opcode): List<OperandKind> {
return when (op) { return when (op) {
Opcode.NOP, Opcode.RET_VOID -> emptyList() 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.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 -> Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT ->
listOf(OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT)
@ -162,6 +162,8 @@ class BytecodeBuilder {
listOf(OperandKind.SLOT, OperandKind.IP) listOf(OperandKind.SLOT, OperandKind.IP)
Opcode.CALL_DIRECT, Opcode.CALL_FALLBACK -> Opcode.CALL_DIRECT, Opcode.CALL_FALLBACK ->
listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_SLOT ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_VIRTUAL -> Opcode.CALL_VIRTUAL ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.GET_FIELD -> Opcode.GET_FIELD ->

View File

@ -18,6 +18,7 @@ package net.sergeych.lyng.bytecode
import net.sergeych.lyng.ExpressionStatement import net.sergeych.lyng.ExpressionStatement
import net.sergeych.lyng.IfStatement import net.sergeych.lyng.IfStatement
import net.sergeych.lyng.ParsedArgument
import net.sergeych.lyng.Pos import net.sergeych.lyng.Pos
import net.sergeych.lyng.Statement import net.sergeych.lyng.Statement
import net.sergeych.lyng.ToBoolStatement import net.sergeych.lyng.ToBoolStatement
@ -70,6 +71,8 @@ class BytecodeCompiler(
is BinaryOpRef -> compileBinary(ref) is BinaryOpRef -> compileBinary(ref)
is UnaryOpRef -> compileUnary(ref) is UnaryOpRef -> compileUnary(ref)
is AssignRef -> compileAssign(ref) is AssignRef -> compileAssign(ref)
is CallRef -> compileCall(ref)
is MethodCallRef -> compileMethodCall(ref)
else -> null else -> null
} }
} }
@ -587,6 +590,64 @@ class BytecodeCompiler(
return CompiledValue(slot, value.type) 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<ParsedArgument>, 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<ParsedArgument>, 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? { private fun compileIf(name: String, stmt: IfStatement): BytecodeFunction? {
val conditionStmt = stmt.condition as? ExpressionStatement ?: return null val conditionStmt = stmt.condition as? ExpressionStatement ?: return null
val condValue = compileRefWithFallback(conditionStmt.ref, SlotType.BOOL, stmt.pos) ?: 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? { private fun compileRefWithFallback(ref: ObjRef, forceType: SlotType?, pos: Pos): CompiledValue? {
val compiled = compileRef(ref) var compiled = compileRef(ref)
if (compiled != null && (forceType == null || compiled.type == forceType || compiled.type == SlotType.UNKNOWN)) { if (compiled != null) {
return if (forceType != null && compiled.type == SlotType.UNKNOWN) { if (forceType == null) return compiled
CompiledValue(compiled.slot, forceType) if (compiled.type == forceType) return compiled
} else compiled if (compiled.type == SlotType.UNKNOWN) {
compiled = null
}
} }
val slot = allocSlot() val slot = allocSlot()
val stmt = if (forceType == SlotType.BOOL) { val stmt = if (forceType == SlotType.BOOL) {
@ -734,9 +797,26 @@ class BytecodeCompiler(
} }
collectScopeSlotsRef(assignValue(ref)) collectScopeSlotsRef(assignValue(ref))
} }
is CallRef -> {
collectScopeSlotsRef(ref.target)
collectScopeSlotsArgs(ref.args)
}
is MethodCallRef -> {
collectScopeSlotsRef(ref.receiver)
collectScopeSlotsArgs(ref.args)
}
else -> {} else -> {}
} }
} }
private fun collectScopeSlotsArgs(args: List<ParsedArgument>) {
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) private data class ScopeSlotKey(val depth: Int, val slot: Int)
} }

View File

@ -83,7 +83,7 @@ object BytecodeDisassembler {
private fun operandKinds(op: Opcode): List<OperandKind> { private fun operandKinds(op: Opcode): List<OperandKind> {
return when (op) { return when (op) {
Opcode.NOP, Opcode.RET_VOID -> emptyList() 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.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 -> Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT ->
listOf(OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT)
@ -115,6 +115,8 @@ object BytecodeDisassembler {
listOf(OperandKind.SLOT, OperandKind.IP) listOf(OperandKind.SLOT, OperandKind.IP)
Opcode.CALL_DIRECT, Opcode.CALL_FALLBACK -> Opcode.CALL_DIRECT, Opcode.CALL_FALLBACK ->
listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_SLOT ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_VIRTUAL -> Opcode.CALL_VIRTUAL ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.GET_FIELD -> Opcode.GET_FIELD ->

View File

@ -30,6 +30,8 @@ data class BytecodeFunction(
val fallbackStatements: List<net.sergeych.lyng.Statement>, val fallbackStatements: List<net.sergeych.lyng.Statement>,
val code: ByteArray, val code: ByteArray,
) { ) {
val methodCallSites: MutableMap<Int, MethodCallSite> = mutableMapOf()
init { init {
require(slotWidth == 1 || slotWidth == 2 || slotWidth == 4) { "slotWidth must be 1,2,4" } require(slotWidth == 1 || slotWidth == 2 || slotWidth == 4) { "slotWidth must be 1,2,4" }
require(ipWidth == 2 || ipWidth == 4) { "ipWidth must be 2 or 4" } require(ipWidth == 2 || ipWidth == 4) { "ipWidth must be 2 or 4" }

View File

@ -16,6 +16,8 @@
package net.sergeych.lyng.bytecode package net.sergeych.lyng.bytecode
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
@ -34,6 +36,7 @@ class BytecodeVm {
var ip = 0 var ip = 0
val code = fn.code val code = fn.code
while (ip < code.size) { while (ip < code.size) {
val startIp = ip
val op = decoder.readOpcode(code, ip) val op = decoder.readOpcode(code, ip)
ip += 1 ip += 1
when (op) { when (op) {
@ -119,6 +122,13 @@ class BytecodeVm {
ip += fn.slotWidth ip += fn.slotWidth
setObj(fn, frame, scope, dst, getObj(fn, frame, scope, src)) 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 -> { Opcode.INT_TO_REAL -> {
val src = decoder.readSlot(code, ip) val src = decoder.readSlot(code, ip)
ip += fn.slotWidth ip += fn.slotWidth
@ -711,6 +721,53 @@ class BytecodeVm {
ip = target 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 -> { Opcode.EVAL_FALLBACK -> {
val id = decoder.readConstId(code, ip, 2) val id = decoder.readConstId(code, ip, 2)
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<Obj>(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 { private fun getObj(fn: BytecodeFunction, frame: BytecodeFrame, scope: Scope, slot: Int): Obj {
return if (slot < fn.scopeSlotCount) { return if (slot < fn.scopeSlotCount) {
resolveScope(scope, fn.scopeSlotDepths[slot]).getSlotRecord(fn.scopeSlotIndices[slot]).value resolveScope(scope, fn.scopeSlotDepths[slot]).getSlotRecord(fn.scopeSlotIndices[slot]).value

View File

@ -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)
}
}

View File

@ -27,6 +27,7 @@ enum class Opcode(val code: Int) {
CONST_REAL(0x07), CONST_REAL(0x07),
CONST_BOOL(0x08), CONST_BOOL(0x08),
CONST_NULL(0x09), CONST_NULL(0x09),
BOX_OBJ(0x0A),
INT_TO_REAL(0x10), INT_TO_REAL(0x10),
REAL_TO_INT(0x11), REAL_TO_INT(0x11),
@ -110,6 +111,7 @@ enum class Opcode(val code: Int) {
CALL_DIRECT(0x90), CALL_DIRECT(0x90),
CALL_VIRTUAL(0x91), CALL_VIRTUAL(0x91),
CALL_FALLBACK(0x92), CALL_FALLBACK(0x92),
CALL_SLOT(0x93),
GET_FIELD(0xA0), GET_FIELD(0xA0),
SET_FIELD(0xA1), SET_FIELD(0xA1),

View File

@ -166,8 +166,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} }
del = del ?: scope.raiseError("Internal error: delegated property $name has no delegate") del = del ?: scope.raiseError("Internal error: delegated property $name has no delegate")
val res = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))) val res = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name)))
obj.value = res return obj.copy(value = res, type = ObjRecord.Type.Other)
return obj
} }
// Map member template to instance storage if applicable // Map member template to instance storage if applicable

View File

@ -1155,6 +1155,17 @@ class FieldRef(
else -> 0L to -1 // no caching for primitives/dynamics without stable shape 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 { override suspend fun evalValue(scope: Scope): Obj {
// Mirror get(), but return raw Obj to avoid transient ObjRecord on R-value paths // Mirror get(), but return raw Obj to avoid transient ObjRecord on R-value paths
val fieldPic = PerfFlags.FIELD_PIC val fieldPic = PerfFlags.FIELD_PIC
@ -1172,14 +1183,14 @@ class FieldRef(
if (key != 0L) { if (key != 0L) {
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (picCounters) PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
return g(base, scope).value return resolveValue(scope, base, g(base, scope))
} } } }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (picCounters) PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
val tK = rKey2; val tV = rVer2; val tG = rGetter2 val tK = rKey2; val tV = rVer2; val tG = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG 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 (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (picCounters) PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
@ -1187,7 +1198,7 @@ class FieldRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG 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 (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (picCounters) PerfStats.fieldPicHit++ if (picCounters) PerfStats.fieldPicHit++
@ -1196,16 +1207,17 @@ class FieldRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG rKey1 = tK; rVer1 = tV; rGetter1 = tG
return g(base, scope).value return resolveValue(scope, base, g(base, scope))
} } } }
if (picCounters) PerfStats.fieldPicMiss++ if (picCounters) PerfStats.fieldPicMiss++
val rec = base.readField(scope, name) val rec = base.readField(scope, name)
// install primary generic getter for this shape // install primary generic getter for this shape
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) } 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). * Direct function call reference: f(args) and optional f?(args).
*/ */
class CallRef( class CallRef(
private val target: ObjRef, internal val target: ObjRef,
private val args: List<ParsedArgument>, internal val args: List<ParsedArgument>,
private val tailBlock: Boolean, internal val tailBlock: Boolean,
private val isOptionalInvoke: Boolean, internal val isOptionalInvoke: Boolean,
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val usePool = PerfFlags.SCOPE_POOL val usePool = PerfFlags.SCOPE_POOL
@ -1592,11 +1604,11 @@ class CallRef(
* Instance method call reference: obj.method(args) and optional obj?.method(args). * Instance method call reference: obj.method(args) and optional obj?.method(args).
*/ */
class MethodCallRef( class MethodCallRef(
private val receiver: ObjRef, internal val receiver: ObjRef,
private val name: String, internal val name: String,
private val args: List<ParsedArgument>, internal val args: List<ParsedArgument>,
private val tailBlock: Boolean, internal val tailBlock: Boolean,
private val isOptional: Boolean, internal val isOptional: Boolean,
) : ObjRef { ) : ObjRef {
// 4-entry PIC for method invocations (guarded by PerfFlags.METHOD_PIC) // 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 private var mKey1: Long = 0L; private var mVer1: Int = -1; private var mInvoker1: (suspend (Obj, Scope, Arguments) -> Obj)? = null

View File

@ -16,7 +16,9 @@
import net.sergeych.lyng.ExpressionStatement import net.sergeych.lyng.ExpressionStatement
import net.sergeych.lyng.IfStatement import net.sergeych.lyng.IfStatement
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
import net.sergeych.lyng.bytecode.BytecodeBuilder import net.sergeych.lyng.bytecode.BytecodeBuilder
import net.sergeych.lyng.bytecode.BytecodeCompiler import net.sergeych.lyng.bytecode.BytecodeCompiler
import net.sergeych.lyng.bytecode.BytecodeConst 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.toBool
import net.sergeych.lyng.obj.toDouble import net.sergeych.lyng.obj.toDouble
import net.sergeych.lyng.obj.toInt import net.sergeych.lyng.obj.toInt
import net.sergeych.lyng.obj.toLong
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -151,6 +154,28 @@ class BytecodeVmTest {
assertEquals(5.75, result.toDouble()) 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 @Test
fun mixedIntRealComparisonUsesBytecodeOps() = kotlinx.coroutines.test.runTest { fun mixedIntRealComparisonUsesBytecodeOps() = kotlinx.coroutines.test.runTest {
val ltExpr = ExpressionStatement( val ltExpr = ExpressionStatement(

View File

@ -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