Fix bytecode call-site semantics
This commit is contained in:
parent
059e366787
commit
72901d9d4c
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -130,7 +130,7 @@ class BytecodeBuilder {
|
||||
private fun operandKinds(op: Opcode): List<OperandKind> {
|
||||
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 ->
|
||||
|
||||
@ -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<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? {
|
||||
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<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)
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ object BytecodeDisassembler {
|
||||
private fun operandKinds(op: Opcode): List<OperandKind> {
|
||||
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 ->
|
||||
|
||||
@ -30,6 +30,8 @@ data class BytecodeFunction(
|
||||
val fallbackStatements: List<net.sergeych.lyng.Statement>,
|
||||
val code: ByteArray,
|
||||
) {
|
||||
val methodCallSites: MutableMap<Int, MethodCallSite> = 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" }
|
||||
|
||||
@ -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<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 {
|
||||
return if (slot < fn.scopeSlotCount) {
|
||||
resolveScope(scope, fn.scopeSlotDepths[slot]).getSlotRecord(fn.scopeSlotIndices[slot]).value
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<ParsedArgument>,
|
||||
private val tailBlock: Boolean,
|
||||
private val isOptionalInvoke: Boolean,
|
||||
internal val target: ObjRef,
|
||||
internal val args: List<ParsedArgument>,
|
||||
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<ParsedArgument>,
|
||||
private val tailBlock: Boolean,
|
||||
private val isOptional: Boolean,
|
||||
internal val receiver: ObjRef,
|
||||
internal val name: String,
|
||||
internal val args: List<ParsedArgument>,
|
||||
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
|
||||
|
||||
@ -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(
|
||||
|
||||
15
notes/bytecode_callsite_fix.md
Normal file
15
notes/bytecode_callsite_fix.md
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user