bytecode: extend call args and cache call sites

This commit is contained in:
Sergey Chernov 2026-01-26 06:33:15 +03:00
parent 144082733c
commit 2f4462858b
13 changed files with 400 additions and 39 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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