Fix bytecode call-site semantics
This commit is contained in:
parent
059e366787
commit
72901d9d4c
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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 ->
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 ->
|
||||||
|
|||||||
@ -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" }
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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_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),
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
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