bytecode: extend call args and cache call sites
This commit is contained in:
parent
144082733c
commit
2f4462858b
@ -32,6 +32,7 @@ slots[localCount .. localCount+argCount-1] arguments
|
|||||||
|
|
||||||
### Constant pool extras
|
### Constant pool extras
|
||||||
- SlotPlan: map of name -> slot index, used by PUSH_SCOPE to pre-allocate and map loop locals.
|
- SlotPlan: map of name -> slot index, used by PUSH_SCOPE to pre-allocate and map loop locals.
|
||||||
|
- CallArgsPlan: ordered argument specs (name/splat) + tailBlock flag, used when argCount has the plan flag set.
|
||||||
|
|
||||||
## 2) Slot ID Width
|
## 2) Slot ID Width
|
||||||
|
|
||||||
@ -83,6 +84,10 @@ Common operand patterns:
|
|||||||
- I: jump target
|
- I: jump target
|
||||||
- F S C S: fnId, argBase slot, argCount, dst slot
|
- F S C S: fnId, argBase slot, argCount, dst slot
|
||||||
|
|
||||||
|
Arg count flag:
|
||||||
|
- If high bit of C is set (0x8000), the low 15 bits encode a CallArgsPlan constId.
|
||||||
|
- When not set, C is the raw positional count and tailBlockMode=false.
|
||||||
|
|
||||||
## 5) Opcode Table
|
## 5) Opcode Table
|
||||||
|
|
||||||
Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass.
|
Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass.
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.util.IdentityHashMap
|
||||||
|
|
||||||
|
internal actual object BytecodeCallSiteCache {
|
||||||
|
private val cache = ThreadLocal.withInitial {
|
||||||
|
IdentityHashMap<BytecodeFunction, MutableMap<Int, MethodCallSite>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun methodCallSites(fn: BytecodeFunction): MutableMap<Int, MethodCallSite> {
|
||||||
|
val map = cache.get()
|
||||||
|
return map.getOrPut(fn) { mutableMapOf() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
internal expect object BytecodeCallSiteCache {
|
||||||
|
fun methodCallSites(fn: BytecodeFunction): MutableMap<Int, MethodCallSite>
|
||||||
|
}
|
||||||
@ -382,7 +382,12 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileCompareEq(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
private fun compileCompareEq(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
||||||
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
|
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) {
|
||||||
|
val left = ensureObjSlot(a)
|
||||||
|
val right = ensureObjSlot(b)
|
||||||
|
builder.emit(Opcode.CMP_EQ_OBJ, left.slot, right.slot, out)
|
||||||
|
return CompiledValue(out, SlotType.BOOL)
|
||||||
|
}
|
||||||
return when {
|
return when {
|
||||||
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
||||||
builder.emit(Opcode.CMP_EQ_INT, a.slot, b.slot, out)
|
builder.emit(Opcode.CMP_EQ_INT, a.slot, b.slot, out)
|
||||||
@ -413,7 +418,12 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileCompareNeq(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
private fun compileCompareNeq(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
||||||
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
|
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) {
|
||||||
|
val left = ensureObjSlot(a)
|
||||||
|
val right = ensureObjSlot(b)
|
||||||
|
builder.emit(Opcode.CMP_NEQ_OBJ, left.slot, right.slot, out)
|
||||||
|
return CompiledValue(out, SlotType.BOOL)
|
||||||
|
}
|
||||||
return when {
|
return when {
|
||||||
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
||||||
builder.emit(Opcode.CMP_NEQ_INT, a.slot, b.slot, out)
|
builder.emit(Opcode.CMP_NEQ_INT, a.slot, b.slot, out)
|
||||||
@ -444,7 +454,12 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileCompareLt(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
private fun compileCompareLt(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
||||||
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
|
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) {
|
||||||
|
val left = ensureObjSlot(a)
|
||||||
|
val right = ensureObjSlot(b)
|
||||||
|
builder.emit(Opcode.CMP_LT_OBJ, left.slot, right.slot, out)
|
||||||
|
return CompiledValue(out, SlotType.BOOL)
|
||||||
|
}
|
||||||
return when {
|
return when {
|
||||||
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
||||||
builder.emit(Opcode.CMP_LT_INT, a.slot, b.slot, out)
|
builder.emit(Opcode.CMP_LT_INT, a.slot, b.slot, out)
|
||||||
@ -471,7 +486,12 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileCompareLte(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
private fun compileCompareLte(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
||||||
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
|
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) {
|
||||||
|
val left = ensureObjSlot(a)
|
||||||
|
val right = ensureObjSlot(b)
|
||||||
|
builder.emit(Opcode.CMP_LTE_OBJ, left.slot, right.slot, out)
|
||||||
|
return CompiledValue(out, SlotType.BOOL)
|
||||||
|
}
|
||||||
return when {
|
return when {
|
||||||
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
||||||
builder.emit(Opcode.CMP_LTE_INT, a.slot, b.slot, out)
|
builder.emit(Opcode.CMP_LTE_INT, a.slot, b.slot, out)
|
||||||
@ -498,7 +518,12 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileCompareGt(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
private fun compileCompareGt(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
||||||
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
|
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) {
|
||||||
|
val left = ensureObjSlot(a)
|
||||||
|
val right = ensureObjSlot(b)
|
||||||
|
builder.emit(Opcode.CMP_GT_OBJ, left.slot, right.slot, out)
|
||||||
|
return CompiledValue(out, SlotType.BOOL)
|
||||||
|
}
|
||||||
return when {
|
return when {
|
||||||
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
||||||
builder.emit(Opcode.CMP_GT_INT, a.slot, b.slot, out)
|
builder.emit(Opcode.CMP_GT_INT, a.slot, b.slot, out)
|
||||||
@ -525,7 +550,12 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileCompareGte(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
private fun compileCompareGte(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
|
||||||
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
|
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) {
|
||||||
|
val left = ensureObjSlot(a)
|
||||||
|
val right = ensureObjSlot(b)
|
||||||
|
builder.emit(Opcode.CMP_GTE_OBJ, left.slot, right.slot, out)
|
||||||
|
return CompiledValue(out, SlotType.BOOL)
|
||||||
|
}
|
||||||
return when {
|
return when {
|
||||||
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
a.type == SlotType.INT && b.type == SlotType.INT -> {
|
||||||
builder.emit(Opcode.CMP_GTE_INT, a.slot, b.slot, out)
|
builder.emit(Opcode.CMP_GTE_INT, a.slot, b.slot, out)
|
||||||
@ -762,62 +792,69 @@ class BytecodeCompiler(
|
|||||||
return CompiledValue(dst, SlotType.OBJ)
|
return CompiledValue(dst, SlotType.OBJ)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class CallArgs(val base: Int, val count: Int)
|
|
||||||
|
|
||||||
private fun compileCall(ref: CallRef): CompiledValue? {
|
private fun compileCall(ref: CallRef): CompiledValue? {
|
||||||
if (ref.isOptionalInvoke) return null
|
if (ref.isOptionalInvoke) return null
|
||||||
if (!argsEligible(ref.args, ref.tailBlock)) return null
|
|
||||||
val callee = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null
|
val callee = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null
|
||||||
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
|
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
|
||||||
|
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||||
val dst = allocSlot()
|
val dst = allocSlot()
|
||||||
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, args.count, dst)
|
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, encodedCount, dst)
|
||||||
return CompiledValue(dst, SlotType.UNKNOWN)
|
return CompiledValue(dst, SlotType.UNKNOWN)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun compileMethodCall(ref: MethodCallRef): CompiledValue? {
|
private fun compileMethodCall(ref: MethodCallRef): CompiledValue? {
|
||||||
if (ref.isOptional) return null
|
if (ref.isOptional) return null
|
||||||
if (!argsEligible(ref.args, ref.tailBlock)) return null
|
|
||||||
val receiver = compileRefWithFallback(ref.receiver, null, Pos.builtIn) ?: return null
|
val receiver = compileRefWithFallback(ref.receiver, null, Pos.builtIn) ?: return null
|
||||||
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
|
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
|
||||||
|
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||||
val methodId = builder.addConst(BytecodeConst.StringVal(ref.name))
|
val methodId = builder.addConst(BytecodeConst.StringVal(ref.name))
|
||||||
if (methodId > 0xFFFF) return null
|
if (methodId > 0xFFFF) return null
|
||||||
val dst = allocSlot()
|
val dst = allocSlot()
|
||||||
builder.emit(Opcode.CALL_VIRTUAL, receiver.slot, methodId, args.base, args.count, dst)
|
builder.emit(Opcode.CALL_VIRTUAL, receiver.slot, methodId, args.base, encodedCount, dst)
|
||||||
return CompiledValue(dst, SlotType.UNKNOWN)
|
return CompiledValue(dst, SlotType.UNKNOWN)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun argsEligible(args: List<ParsedArgument>, tailBlock: Boolean): Boolean {
|
private data class CallArgs(val base: Int, val count: Int, val planId: Int?)
|
||||||
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? {
|
private fun compileCallArgs(args: List<ParsedArgument>, tailBlock: Boolean): CallArgs? {
|
||||||
if (tailBlock) return null
|
if (args.isEmpty()) return CallArgs(base = 0, count = 0, planId = 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() }
|
val argSlots = IntArray(args.size) { allocSlot() }
|
||||||
|
val needPlan = tailBlock || args.any { it.isSplat || it.name != null }
|
||||||
|
val specs = if (needPlan) ArrayList<BytecodeConst.CallArgSpec>(args.size) else null
|
||||||
for ((index, arg) in args.withIndex()) {
|
for ((index, arg) in args.withIndex()) {
|
||||||
val stmt = arg.value
|
val compiled = compileArgValue(arg.value) ?: return null
|
||||||
val compiled = if (stmt is ExpressionStatement) {
|
|
||||||
compileRefWithFallback(stmt.ref, null, stmt.pos)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
} ?: return null
|
|
||||||
val dst = argSlots[index]
|
val dst = argSlots[index]
|
||||||
if (compiled.slot != dst) {
|
if (compiled.slot != dst || compiled.type != SlotType.OBJ) {
|
||||||
builder.emit(Opcode.BOX_OBJ, compiled.slot, dst)
|
|
||||||
} else if (compiled.type != SlotType.OBJ) {
|
|
||||||
builder.emit(Opcode.BOX_OBJ, compiled.slot, dst)
|
builder.emit(Opcode.BOX_OBJ, compiled.slot, dst)
|
||||||
}
|
}
|
||||||
updateSlotType(dst, SlotType.OBJ)
|
updateSlotType(dst, SlotType.OBJ)
|
||||||
|
specs?.add(BytecodeConst.CallArgSpec(arg.name, arg.isSplat))
|
||||||
}
|
}
|
||||||
return CallArgs(base = argSlots[0], count = argSlots.size)
|
val planId = if (needPlan) {
|
||||||
|
builder.addConst(BytecodeConst.CallArgsPlan(tailBlock, specs ?: emptyList()))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return CallArgs(base = argSlots[0], count = argSlots.size, planId = planId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compileArgValue(stmt: Statement): CompiledValue? {
|
||||||
|
return when (stmt) {
|
||||||
|
is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos)
|
||||||
|
else -> {
|
||||||
|
val slot = allocSlot()
|
||||||
|
val id = builder.addFallback(stmt)
|
||||||
|
builder.emit(Opcode.EVAL_FALLBACK, id, slot)
|
||||||
|
updateSlotType(slot, SlotType.OBJ)
|
||||||
|
CompiledValue(slot, SlotType.OBJ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun encodeCallArgCount(args: CallArgs): Int? {
|
||||||
|
val planId = args.planId ?: return args.count
|
||||||
|
if (planId > 0x7FFF) return null
|
||||||
|
return 0x8000 or planId
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun compileIf(name: String, stmt: IfStatement): BytecodeFunction? {
|
private fun compileIf(name: String, stmt: IfStatement): BytecodeFunction? {
|
||||||
|
|||||||
@ -26,4 +26,6 @@ sealed class BytecodeConst {
|
|||||||
data class StringVal(val value: String) : BytecodeConst()
|
data class StringVal(val value: String) : BytecodeConst()
|
||||||
data class ObjRef(val value: Obj) : BytecodeConst()
|
data class ObjRef(val value: Obj) : BytecodeConst()
|
||||||
data class SlotPlan(val plan: Map<String, Int>) : BytecodeConst()
|
data class SlotPlan(val plan: Map<String, Int>) : BytecodeConst()
|
||||||
|
data class CallArgsPlan(val tailBlock: Boolean, val specs: List<CallArgSpec>) : BytecodeConst()
|
||||||
|
data class CallArgSpec(val name: String?, val isSplat: Boolean)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,8 +30,6 @@ 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" }
|
||||||
|
|||||||
@ -22,9 +22,15 @@ import net.sergeych.lyng.Scope
|
|||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
|
|
||||||
class BytecodeVm {
|
class BytecodeVm {
|
||||||
|
companion object {
|
||||||
|
private const val ARG_PLAN_FLAG = 0x8000
|
||||||
|
private const val ARG_PLAN_MASK = 0x7FFF
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun execute(fn: BytecodeFunction, scope0: Scope, args: List<Obj>): Obj {
|
suspend fun execute(fn: BytecodeFunction, scope0: Scope, args: List<Obj>): Obj {
|
||||||
val scopeStack = ArrayDeque<Scope>()
|
val scopeStack = ArrayDeque<Scope>()
|
||||||
var scope = scope0
|
var scope = scope0
|
||||||
|
val methodCallSites = BytecodeCallSiteCache.methodCallSites(fn)
|
||||||
val frame = BytecodeFrame(fn.localCount, args.size)
|
val frame = BytecodeFrame(fn.localCount, args.size)
|
||||||
for (i in args.indices) {
|
for (i in args.indices) {
|
||||||
frame.setObj(frame.argBase + i, args[i])
|
frame.setObj(frame.argBase + i, args[i])
|
||||||
@ -776,7 +782,7 @@ class BytecodeVm {
|
|||||||
val nameConst = fn.constants.getOrNull(methodId) as? BytecodeConst.StringVal
|
val nameConst = fn.constants.getOrNull(methodId) as? BytecodeConst.StringVal
|
||||||
?: error("CALL_VIRTUAL expects StringVal at $methodId")
|
?: error("CALL_VIRTUAL expects StringVal at $methodId")
|
||||||
val args = buildArguments(fn, frame, scope, argBase, argCount)
|
val args = buildArguments(fn, frame, scope, argBase, argCount)
|
||||||
val site = fn.methodCallSites.getOrPut(startIp) { MethodCallSite(nameConst.value) }
|
val site = methodCallSites.getOrPut(startIp) { MethodCallSite(nameConst.value) }
|
||||||
val result = site.invoke(scope, receiver, args)
|
val result = site.invoke(scope, receiver, args)
|
||||||
when (result) {
|
when (result) {
|
||||||
is ObjInt -> setInt(fn, frame, scope, dst, result.value)
|
is ObjInt -> setInt(fn, frame, scope, dst, result.value)
|
||||||
@ -825,7 +831,7 @@ class BytecodeVm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildArguments(
|
private suspend fun buildArguments(
|
||||||
fn: BytecodeFunction,
|
fn: BytecodeFunction,
|
||||||
frame: BytecodeFrame,
|
frame: BytecodeFrame,
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
@ -833,6 +839,12 @@ class BytecodeVm {
|
|||||||
argCount: Int,
|
argCount: Int,
|
||||||
): Arguments {
|
): Arguments {
|
||||||
if (argCount == 0) return Arguments.EMPTY
|
if (argCount == 0) return Arguments.EMPTY
|
||||||
|
if ((argCount and ARG_PLAN_FLAG) != 0) {
|
||||||
|
val planId = argCount and ARG_PLAN_MASK
|
||||||
|
val plan = fn.constants.getOrNull(planId) as? BytecodeConst.CallArgsPlan
|
||||||
|
?: error("CALL args plan not found: $planId")
|
||||||
|
return buildArgumentsFromPlan(fn, frame, scope, argBase, plan)
|
||||||
|
}
|
||||||
val list = ArrayList<Obj>(argCount)
|
val list = ArrayList<Obj>(argCount)
|
||||||
for (i in 0 until argCount) {
|
for (i in 0 until argCount) {
|
||||||
list.add(slotToObj(fn, frame, scope, argBase + i))
|
list.add(slotToObj(fn, frame, scope, argBase + i))
|
||||||
@ -840,6 +852,62 @@ class BytecodeVm {
|
|||||||
return Arguments(list)
|
return Arguments(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun buildArgumentsFromPlan(
|
||||||
|
fn: BytecodeFunction,
|
||||||
|
frame: BytecodeFrame,
|
||||||
|
scope: Scope,
|
||||||
|
argBase: Int,
|
||||||
|
plan: BytecodeConst.CallArgsPlan,
|
||||||
|
): Arguments {
|
||||||
|
val positional = ArrayList<Obj>(plan.specs.size)
|
||||||
|
var named: LinkedHashMap<String, Obj>? = null
|
||||||
|
var namedSeen = false
|
||||||
|
for ((idx, spec) in plan.specs.withIndex()) {
|
||||||
|
val value = slotToObj(fn, frame, scope, argBase + idx)
|
||||||
|
val name = spec.name
|
||||||
|
if (name != null) {
|
||||||
|
if (named == null) named = linkedMapOf()
|
||||||
|
if (named.containsKey(name)) scope.raiseIllegalArgument("argument '$name' is already set")
|
||||||
|
named[name] = value
|
||||||
|
namedSeen = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (spec.isSplat) {
|
||||||
|
when {
|
||||||
|
value is ObjMap -> {
|
||||||
|
if (named == null) named = linkedMapOf()
|
||||||
|
for ((k, v) in value.map) {
|
||||||
|
if (k !is ObjString) scope.raiseIllegalArgument("named splat expects a Map with string keys")
|
||||||
|
val key = k.value
|
||||||
|
if (named.containsKey(key)) scope.raiseIllegalArgument("argument '$key' is already set")
|
||||||
|
named[key] = v
|
||||||
|
}
|
||||||
|
namedSeen = true
|
||||||
|
}
|
||||||
|
value is ObjList -> {
|
||||||
|
if (namedSeen) scope.raiseIllegalArgument("positional splat cannot follow named arguments")
|
||||||
|
positional.addAll(value.list)
|
||||||
|
}
|
||||||
|
value.isInstanceOf(ObjIterable) -> {
|
||||||
|
if (namedSeen) scope.raiseIllegalArgument("positional splat cannot follow named arguments")
|
||||||
|
val list = (value.invokeInstanceMethod(scope, "toList") as ObjList).list
|
||||||
|
positional.addAll(list)
|
||||||
|
}
|
||||||
|
else -> scope.raiseClassCastError("expected list of objects for splat argument")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (namedSeen) {
|
||||||
|
val isLast = idx == plan.specs.lastIndex
|
||||||
|
if (!(isLast && plan.tailBlock)) {
|
||||||
|
scope.raiseIllegalArgument("positional argument cannot follow named arguments")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
positional.add(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Arguments(positional, plan.tailBlock, named ?: emptyMap())
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import net.sergeych.lyng.bytecode.BytecodeVm
|
|||||||
import net.sergeych.lyng.bytecode.Opcode
|
import net.sergeych.lyng.bytecode.Opcode
|
||||||
import net.sergeych.lyng.obj.BinaryOpRef
|
import net.sergeych.lyng.obj.BinaryOpRef
|
||||||
import net.sergeych.lyng.obj.BinOp
|
import net.sergeych.lyng.obj.BinOp
|
||||||
|
import net.sergeych.lyng.obj.CallRef
|
||||||
import net.sergeych.lyng.obj.ConstRef
|
import net.sergeych.lyng.obj.ConstRef
|
||||||
import net.sergeych.lyng.obj.LocalSlotRef
|
import net.sergeych.lyng.obj.LocalSlotRef
|
||||||
import net.sergeych.lyng.obj.ObjFalse
|
import net.sergeych.lyng.obj.ObjFalse
|
||||||
@ -203,6 +204,81 @@ class BytecodeVmTest {
|
|||||||
assertEquals(true, eqResult.toBool())
|
assertEquals(true, eqResult.toBool())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun callWithTailBlockKeepsTailBlockMode() = kotlinx.coroutines.test.runTest {
|
||||||
|
val callable = object : Statement() {
|
||||||
|
override val pos: Pos = Pos.builtIn
|
||||||
|
override suspend fun execute(scope: Scope) =
|
||||||
|
if (scope.args.tailBlockMode) ObjTrue else ObjFalse
|
||||||
|
}
|
||||||
|
val callRef = CallRef(
|
||||||
|
ConstRef(callable.asReadonly),
|
||||||
|
listOf(
|
||||||
|
net.sergeych.lyng.ParsedArgument(
|
||||||
|
ExpressionStatement(ConstRef(ObjInt.of(1).asReadonly), Pos.builtIn),
|
||||||
|
Pos.builtIn
|
||||||
|
)
|
||||||
|
),
|
||||||
|
tailBlock = true,
|
||||||
|
isOptionalInvoke = false
|
||||||
|
)
|
||||||
|
val expr = ExpressionStatement(callRef, Pos.builtIn)
|
||||||
|
val fn = BytecodeCompiler().compileExpression("tailBlockArgs", expr) ?: error("bytecode compile failed")
|
||||||
|
val result = BytecodeVm().execute(fn, Scope(), emptyList())
|
||||||
|
assertEquals(true, result.toBool())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun callWithNamedArgumentsUsesPlan() = kotlinx.coroutines.test.runTest {
|
||||||
|
val callable = object : Statement() {
|
||||||
|
override val pos: Pos = Pos.builtIn
|
||||||
|
override suspend fun execute(scope: Scope) =
|
||||||
|
(scope.args.named["x"] as ObjInt)
|
||||||
|
}
|
||||||
|
val callRef = CallRef(
|
||||||
|
ConstRef(callable.asReadonly),
|
||||||
|
listOf(
|
||||||
|
net.sergeych.lyng.ParsedArgument(
|
||||||
|
ExpressionStatement(ConstRef(ObjInt.of(5).asReadonly), Pos.builtIn),
|
||||||
|
Pos.builtIn,
|
||||||
|
name = "x"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
tailBlock = false,
|
||||||
|
isOptionalInvoke = false
|
||||||
|
)
|
||||||
|
val expr = ExpressionStatement(callRef, Pos.builtIn)
|
||||||
|
val fn = BytecodeCompiler().compileExpression("namedArgs", expr) ?: error("bytecode compile failed")
|
||||||
|
val result = BytecodeVm().execute(fn, Scope(), emptyList())
|
||||||
|
assertEquals(5, result.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun callWithSplatArgumentsUsesPlan() = kotlinx.coroutines.test.runTest {
|
||||||
|
val callable = object : Statement() {
|
||||||
|
override val pos: Pos = Pos.builtIn
|
||||||
|
override suspend fun execute(scope: Scope) =
|
||||||
|
ObjInt.of(scope.args.size.toLong())
|
||||||
|
}
|
||||||
|
val list = ObjList(mutableListOf<net.sergeych.lyng.obj.Obj>(ObjInt.of(1), ObjInt.of(2), ObjInt.of(3)))
|
||||||
|
val callRef = CallRef(
|
||||||
|
ConstRef(callable.asReadonly),
|
||||||
|
listOf(
|
||||||
|
net.sergeych.lyng.ParsedArgument(
|
||||||
|
ExpressionStatement(ConstRef(list.asReadonly), Pos.builtIn),
|
||||||
|
Pos.builtIn,
|
||||||
|
isSplat = true
|
||||||
|
)
|
||||||
|
),
|
||||||
|
tailBlock = false,
|
||||||
|
isOptionalInvoke = false
|
||||||
|
)
|
||||||
|
val expr = ExpressionStatement(callRef, Pos.builtIn)
|
||||||
|
val fn = BytecodeCompiler().compileExpression("splatArgs", expr) ?: error("bytecode compile failed")
|
||||||
|
val result = BytecodeVm().execute(fn, Scope(), emptyList())
|
||||||
|
assertEquals(3, result.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun mixedIntRealArithmeticUsesBytecodeOps() = kotlinx.coroutines.test.runTest {
|
fun mixedIntRealArithmeticUsesBytecodeOps() = kotlinx.coroutines.test.runTest {
|
||||||
val expr = ExpressionStatement(
|
val expr = ExpressionStatement(
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
internal actual object BytecodeCallSiteCache {
|
||||||
|
private val cache = mutableMapOf<BytecodeFunction, MutableMap<Int, MethodCallSite>>()
|
||||||
|
|
||||||
|
actual fun methodCallSites(fn: BytecodeFunction): MutableMap<Int, MethodCallSite> {
|
||||||
|
return cache.getOrPut(fn) { mutableMapOf() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.util.IdentityHashMap
|
||||||
|
|
||||||
|
internal actual object BytecodeCallSiteCache {
|
||||||
|
private val cache = ThreadLocal.withInitial {
|
||||||
|
IdentityHashMap<BytecodeFunction, MutableMap<Int, MethodCallSite>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun methodCallSites(fn: BytecodeFunction): MutableMap<Int, MethodCallSite> {
|
||||||
|
val map = cache.get()
|
||||||
|
return map.getOrPut(fn) { mutableMapOf() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
@kotlin.native.concurrent.ThreadLocal
|
||||||
|
internal actual object BytecodeCallSiteCache {
|
||||||
|
private val cache = mutableMapOf<BytecodeFunction, MutableMap<Int, MethodCallSite>>()
|
||||||
|
|
||||||
|
actual fun methodCallSites(fn: BytecodeFunction): MutableMap<Int, MethodCallSite> {
|
||||||
|
return cache.getOrPut(fn) { mutableMapOf() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
internal actual object BytecodeCallSiteCache {
|
||||||
|
private val cache = mutableMapOf<BytecodeFunction, MutableMap<Int, MethodCallSite>>()
|
||||||
|
|
||||||
|
actual fun methodCallSites(fn: BytecodeFunction): MutableMap<Int, MethodCallSite> {
|
||||||
|
return cache.getOrPut(fn) { mutableMapOf() }
|
||||||
|
}
|
||||||
|
}
|
||||||
18
notes/bytecode_callsite_cache.md
Normal file
18
notes/bytecode_callsite_cache.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Bytecode method call-site cache
|
||||||
|
|
||||||
|
Changes
|
||||||
|
- Added per-thread bytecode method call-site caches via BytecodeCallSiteCache expect/actuals.
|
||||||
|
- Bytecode VM now reuses per-function call-site maps to preserve method PIC hits across repeated bytecode executions.
|
||||||
|
- Removed unused methodCallSites property from BytecodeFunction.
|
||||||
|
|
||||||
|
Why
|
||||||
|
- Fixes JVM PIC invalidation test by allowing method PIC hits when bytecode bodies are invoked repeatedly (e.g., loop bodies compiled to bytecode statements).
|
||||||
|
- Avoids cross-thread mutable map sharing on native by using thread-local storage.
|
||||||
|
|
||||||
|
Tests
|
||||||
|
- ./gradlew :lynglib:jvmTest
|
||||||
|
- ./gradlew :lynglib:allTests -x :lynglib:jvmTest
|
||||||
|
|
||||||
|
Benchmark
|
||||||
|
- ./gradlew :lynglib:jvmTest --tests NestedRangeBenchmarkTest -Dbenchmarks=true
|
||||||
|
- nested-happy elapsed=1266 ms
|
||||||
Loading…
x
Reference in New Issue
Block a user