Enforce compile-time only member access

This commit is contained in:
Sergey Chernov 2026-02-04 03:30:56 +03:00
parent 862486e0e8
commit 47654aee38
18 changed files with 333 additions and 714 deletions

View File

@ -1,30 +0,0 @@
/*
* 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 CmdCallSiteCache {
private val cache = ThreadLocal.withInitial {
IdentityHashMap<CmdFunction, MutableMap<Int, MethodCallSite>>()
}
actual fun methodCallSites(fn: CmdFunction): MutableMap<Int, MethodCallSite> {
val map = cache.get()
return map.getOrPut(fn) { mutableMapOf() }
}
}

View File

@ -1079,7 +1079,9 @@ class Compiler(
block,
"<script>",
allowLocalSlots = true,
allowedScopeNames = modulePlan.keys
allowedScopeNames = modulePlan.keys,
slotTypeByScopeId = slotTypeByScopeId,
knownNameObjClass = knownClassMapForBytecode()
)
)
} else {
@ -1284,6 +1286,27 @@ class Compiler(
}
}
private fun knownClassMapForBytecode(): Map<String, ObjClass> {
val result = LinkedHashMap<String, ObjClass>()
fun addScope(scope: Scope?) {
if (scope == null) return
for ((name, rec) in scope.objects) {
val cls = rec.value as? ObjClass ?: continue
result.putIfAbsent(name, cls)
}
}
addScope(seedScope)
addScope(importManager.rootScope)
for (scope in importedScopes) {
addScope(scope)
}
for (name in compileClassInfos.keys) {
val cls = resolveClassByName(name) ?: continue
result.putIfAbsent(name, cls)
}
return result
}
private fun wrapBytecode(stmt: Statement): Statement {
if (!useBytecodeStatements) return stmt
if (codeContexts.lastOrNull() is CodeContext.Module) {
@ -1322,7 +1345,9 @@ class Compiler(
allowLocalSlots = allowLocals,
returnLabels = returnLabels,
rangeLocalNames = currentRangeParamNames,
allowedScopeNames = allowedScopeNames
allowedScopeNames = allowedScopeNames,
slotTypeByScopeId = slotTypeByScopeId,
knownNameObjClass = knownClassMapForBytecode()
)
}
@ -1338,7 +1363,9 @@ class Compiler(
allowLocalSlots = true,
returnLabels = returnLabels,
rangeLocalNames = currentRangeParamNames,
allowedScopeNames = allowedScopeNames
allowedScopeNames = allowedScopeNames,
slotTypeByScopeId = slotTypeByScopeId,
knownNameObjClass = knownClassMapForBytecode()
)
}
@ -3250,8 +3277,7 @@ class Compiler(
if (left is LocalSlotRef && left.name == "scope") return
val receiverClass = resolveReceiverClassForMember(left)
if (receiverClass == null) {
val allowed = memberName == "toString" || memberName == "toInspectString"
if (allowed) return
if (isAllowedObjectMember(memberName)) return
throw ScriptError(pos, "member access requires compile-time receiver type: $memberName")
}
if (receiverClass == Obj.rootObjectType) {

View File

@ -19,7 +19,7 @@ package net.sergeych.lyng.bytecode
import net.sergeych.lyng.Pos
class BytecodeFallbackException(
class BytecodeCompileException(
message: String,
val pos: Pos? = null,
) : RuntimeException(message) {

View File

@ -42,6 +42,8 @@ class BytecodeCompiler(
private val returnLabels: Set<String> = emptySet(),
private val rangeLocalNames: Set<String> = emptySet(),
private val allowedScopeNames: Set<String>? = null,
private val slotTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
private val knownNameObjClass: Map<String, ObjClass> = emptyMap(),
) {
private var builder = CmdBuilder()
private var nextSlot = 0
@ -281,7 +283,7 @@ class BytecodeCompiler(
is ThisMethodSlotCallRef -> compileThisMethodSlotCall(ref)
is StatementRef -> {
val compiled = compileStatementValueOrFallback(ref.statement)
compiled ?: throw BytecodeFallbackException(
compiled ?: throw BytecodeCompileException(
"Unsupported StatementRef(${ref.statement::class.simpleName})",
Pos.builtIn
)
@ -309,9 +311,9 @@ class BytecodeCompiler(
val methodId = ref.methodId ?: -1
if (fieldId < 0 && methodId < 0) {
val typeName = ref.preferredThisTypeName()
?: throw BytecodeFallbackException("Missing member id for ${ref.name}", Pos.builtIn)
?: throw BytecodeCompileException("Missing member id for ${ref.name}", Pos.builtIn)
val wrapperName = extensionPropertyGetterName(typeName, ref.name)
val callee = resolveDirectNameSlot(wrapperName) ?: throw BytecodeFallbackException(
val callee = resolveDirectNameSlot(wrapperName) ?: throw BytecodeCompileException(
"Missing extension wrapper for ${typeName}.${ref.name}",
Pos.builtIn
)
@ -367,12 +369,12 @@ class BytecodeCompiler(
builder.mark(endLabel)
return CompiledValue(dst, SlotType.OBJ)
}
val typeName = ref.preferredThisTypeName() ?: throw BytecodeFallbackException(
val typeName = ref.preferredThisTypeName() ?: throw BytecodeCompileException(
"Missing member id for ${ref.methodName()}",
Pos.builtIn
)
val wrapperName = extensionCallableName(typeName, ref.methodName())
val callee = resolveDirectNameSlot(wrapperName) ?: throw BytecodeFallbackException(
val callee = resolveDirectNameSlot(wrapperName) ?: throw BytecodeCompileException(
"Missing extension wrapper for ${typeName}.${ref.methodName()}",
Pos.builtIn
)
@ -468,7 +470,7 @@ class BytecodeCompiler(
is UnaryOpRef -> "UnaryOpRef(${ref.op})"
else -> ref::class.simpleName ?: "UnknownRef"
}
throw BytecodeFallbackException("Unsupported expression ($refInfo)", Pos.builtIn)
throw BytecodeCompileException("Unsupported expression ($refInfo)", Pos.builtIn)
}
private fun compileListLiteral(ref: ListLiteralRef): CompiledValue? {
@ -513,7 +515,7 @@ class BytecodeCompiler(
builder.emit(Opcode.SET_INDEX, dst, keySlot, value.slot)
}
is net.sergeych.lyng.obj.MapLiteralEntry.Spread -> {
throw BytecodeFallbackException("Map spread is not supported in bytecode", Pos.builtIn)
throw BytecodeCompileException("Map spread is not supported in bytecode", Pos.builtIn)
}
}
}
@ -563,16 +565,7 @@ class BytecodeCompiler(
builder.emit(Opcode.NEG_REAL, a.slot, out)
CompiledValue(out, SlotType.REAL)
}
else -> {
val zeroId = builder.addConst(BytecodeConst.IntVal(0))
val zeroSlot = allocSlot()
builder.emit(Opcode.CONST_INT, zeroId, zeroSlot)
updateSlotType(zeroSlot, SlotType.INT)
val obj = ensureObjSlot(a)
builder.emit(Opcode.SUB_OBJ, zeroSlot, obj.slot, out)
updateSlotType(out, SlotType.OBJ)
CompiledValue(out, SlotType.OBJ)
}
else -> compileObjUnaryOp(unaryOperand(ref), a, "negate", Pos.builtIn)
}
UnaryOp.NOT -> {
when (a.type) {
@ -598,11 +591,154 @@ class BytecodeCompiler(
builder.emit(Opcode.INV_INT, a.slot, out)
return CompiledValue(out, SlotType.INT)
}
return compileEvalRef(ref)
return compileObjUnaryOp(unaryOperand(ref), a, "bitNot", Pos.builtIn)
}
}
}
private fun compileObjUnaryOp(
ref: ObjRef,
value: CompiledValue,
memberName: String,
pos: Pos
): CompiledValue? {
val receiverClass = resolveReceiverClass(ref)
val methodId = receiverClass?.instanceMethodIdMap(includeAbstract = true)?.get(memberName)
if (methodId != null) {
val receiverObj = ensureObjSlot(value)
val dst = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, receiverObj.slot, methodId, 0, 0, dst)
updateSlotType(dst, SlotType.OBJ)
return CompiledValue(dst, SlotType.OBJ)
}
if (receiverClass == null && memberName == "negate") {
val zeroId = builder.addConst(BytecodeConst.IntVal(0))
val zeroSlot = allocSlot()
builder.emit(Opcode.CONST_INT, zeroId, zeroSlot)
updateSlotType(zeroSlot, SlotType.INT)
val obj = ensureObjSlot(value)
val dst = allocSlot()
builder.emit(Opcode.SUB_OBJ, zeroSlot, obj.slot, dst)
updateSlotType(dst, SlotType.OBJ)
return CompiledValue(dst, SlotType.OBJ)
}
if (memberName == "negate" && receiverClass in setOf(ObjInt.type, ObjReal.type)) {
val zeroId = builder.addConst(BytecodeConst.IntVal(0))
val zeroSlot = allocSlot()
builder.emit(Opcode.CONST_INT, zeroId, zeroSlot)
updateSlotType(zeroSlot, SlotType.INT)
val obj = ensureObjSlot(value)
val dst = allocSlot()
builder.emit(Opcode.SUB_OBJ, zeroSlot, obj.slot, dst)
updateSlotType(dst, SlotType.OBJ)
return CompiledValue(dst, SlotType.OBJ)
}
throw BytecodeCompileException(
"Unknown member $memberName on ${receiverClass?.className ?: "unknown"}",
pos
)
}
private fun operatorMemberName(op: BinOp): String? = when (op) {
BinOp.PLUS -> "plus"
BinOp.MINUS -> "minus"
BinOp.STAR -> "mul"
BinOp.SLASH -> "div"
BinOp.PERCENT -> "mod"
BinOp.BAND -> "bitAnd"
BinOp.BOR -> "bitOr"
BinOp.BXOR -> "bitXor"
BinOp.SHL -> "shl"
BinOp.SHR -> "shr"
else -> null
}
private fun allowKotlinOperatorFallback(receiverClass: ObjClass, op: BinOp): Boolean = when (op) {
BinOp.PLUS -> receiverClass in setOf(
ObjString.type,
ObjInt.type,
ObjReal.type,
ObjList.type,
ObjSet.type,
ObjMap.type,
ObjBuffer.type,
ObjInstant.type,
ObjDateTime.type
)
BinOp.MINUS -> receiverClass in setOf(
ObjInt.type,
ObjReal.type,
ObjSet.type,
ObjInstant.type,
ObjDateTime.type
)
BinOp.STAR -> receiverClass in setOf(ObjInt.type, ObjReal.type, ObjString.type)
BinOp.SLASH, BinOp.PERCENT -> receiverClass in setOf(ObjInt.type, ObjReal.type)
else -> false
}
private fun compileObjBinaryOp(
leftRef: ObjRef,
leftValue: CompiledValue,
rightValue: CompiledValue,
op: BinOp,
pos: Pos
): CompiledValue? {
val memberName = operatorMemberName(op) ?: return null
val receiverClass = resolveReceiverClass(leftRef)
if (receiverClass == null) {
val objOpcode = when (op) {
BinOp.PLUS -> Opcode.ADD_OBJ
BinOp.MINUS -> Opcode.SUB_OBJ
BinOp.STAR -> Opcode.MUL_OBJ
BinOp.SLASH -> Opcode.DIV_OBJ
BinOp.PERCENT -> Opcode.MOD_OBJ
else -> null
}
if (objOpcode != null) {
val receiverObj = ensureObjSlot(leftValue)
val argObj = ensureObjSlot(rightValue)
val dst = allocSlot()
builder.emit(objOpcode, receiverObj.slot, argObj.slot, dst)
updateSlotType(dst, SlotType.OBJ)
return CompiledValue(dst, SlotType.OBJ)
}
throw BytecodeCompileException(
"Operator requires compile-time receiver type: $memberName",
pos
)
}
val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[memberName]
if (methodId != null) {
val receiverObj = ensureObjSlot(leftValue)
val argObj = ensureObjSlot(rightValue)
val dst = allocSlot()
builder.emit(Opcode.CALL_MEMBER_SLOT, receiverObj.slot, methodId, argObj.slot, 1, dst)
updateSlotType(dst, SlotType.OBJ)
return CompiledValue(dst, SlotType.OBJ)
}
val objOpcode = when (op) {
BinOp.PLUS -> Opcode.ADD_OBJ
BinOp.MINUS -> Opcode.SUB_OBJ
BinOp.STAR -> Opcode.MUL_OBJ
BinOp.SLASH -> Opcode.DIV_OBJ
BinOp.PERCENT -> Opcode.MOD_OBJ
else -> null
}
if (objOpcode != null && allowKotlinOperatorFallback(receiverClass, op)) {
val receiverObj = ensureObjSlot(leftValue)
val argObj = ensureObjSlot(rightValue)
val dst = allocSlot()
builder.emit(objOpcode, receiverObj.slot, argObj.slot, dst)
updateSlotType(dst, SlotType.OBJ)
return CompiledValue(dst, SlotType.OBJ)
}
throw BytecodeCompileException(
"Unknown member $memberName on ${receiverClass.className}",
pos
)
}
private fun compileBinary(ref: BinaryOpRef): CompiledValue? {
val op = binaryOp(ref)
if (op == BinOp.AND || op == BinOp.OR) {
@ -712,12 +848,12 @@ class BytecodeCompiler(
if (op == BinOp.MATCH || op == BinOp.NOTMATCH) {
val leftRef = binaryLeft(ref)
val rightRef = binaryRight(ref)
val receiverClass = resolveReceiverClass(leftRef) ?: throw BytecodeFallbackException(
val receiverClass = resolveReceiverClass(leftRef) ?: throw BytecodeCompileException(
"Match operator requires compile-time receiver type",
refPos(ref)
)
val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)["operatorMatch"]
?: throw BytecodeFallbackException(
?: throw BytecodeCompileException(
"Unknown member operatorMatch on ${receiverClass.className}",
refPos(ref)
)
@ -782,36 +918,12 @@ class BytecodeCompiler(
val typesMismatch = a.type != b.type && a.type != SlotType.UNKNOWN && b.type != SlotType.UNKNOWN
val allowMixedNumeric = op in setOf(BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH)
if (typesMismatch && op in setOf(BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT)) {
val leftObj = ensureObjSlot(a)
val rightObj = ensureObjSlot(b)
val out = allocSlot()
val objOpcode = when (op) {
BinOp.PLUS -> Opcode.ADD_OBJ
BinOp.MINUS -> Opcode.SUB_OBJ
BinOp.STAR -> Opcode.MUL_OBJ
BinOp.SLASH -> Opcode.DIV_OBJ
BinOp.PERCENT -> Opcode.MOD_OBJ
else -> null
} ?: return null
builder.emit(objOpcode, leftObj.slot, rightObj.slot, out)
return CompiledValue(out, SlotType.OBJ)
return compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
}
if ((a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) &&
op in setOf(BinOp.PLUS, BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT)
) {
val leftObj = ensureObjSlot(a)
val rightObj = ensureObjSlot(b)
val out = allocSlot()
val objOpcode = when (op) {
BinOp.PLUS -> Opcode.ADD_OBJ
BinOp.MINUS -> Opcode.SUB_OBJ
BinOp.STAR -> Opcode.MUL_OBJ
BinOp.SLASH -> Opcode.DIV_OBJ
BinOp.PERCENT -> Opcode.MOD_OBJ
else -> null
} ?: return null
builder.emit(objOpcode, leftObj.slot, rightObj.slot, out)
return CompiledValue(out, SlotType.OBJ)
return compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
}
if (typesMismatch && !allowMixedNumeric &&
op !in setOf(BinOp.EQ, BinOp.NEQ, BinOp.LT, BinOp.LTE, BinOp.GT, BinOp.GTE)
@ -844,9 +956,8 @@ class BytecodeCompiler(
}
}
SlotType.OBJ -> {
if (b.type != SlotType.OBJ) return null
builder.emit(Opcode.ADD_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.OBJ)
if (b.type != SlotType.OBJ && b.type != SlotType.UNKNOWN) return null
compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
}
else -> null
}
@ -874,9 +985,8 @@ class BytecodeCompiler(
}
}
SlotType.OBJ -> {
if (b.type != SlotType.OBJ) return null
builder.emit(Opcode.SUB_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.OBJ)
if (b.type != SlotType.OBJ && b.type != SlotType.UNKNOWN) return null
compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
}
else -> null
}
@ -904,9 +1014,8 @@ class BytecodeCompiler(
}
}
SlotType.OBJ -> {
if (b.type != SlotType.OBJ) return null
builder.emit(Opcode.MUL_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.OBJ)
if (b.type != SlotType.OBJ && b.type != SlotType.UNKNOWN) return null
compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
}
else -> null
}
@ -934,9 +1043,8 @@ class BytecodeCompiler(
}
}
SlotType.OBJ -> {
if (b.type != SlotType.OBJ) return null
builder.emit(Opcode.DIV_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.OBJ)
if (b.type != SlotType.OBJ && b.type != SlotType.UNKNOWN) return null
compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
}
else -> null
}
@ -948,9 +1056,8 @@ class BytecodeCompiler(
CompiledValue(out, SlotType.INT)
}
SlotType.OBJ -> {
if (b.type != SlotType.OBJ) return null
builder.emit(Opcode.MOD_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.OBJ)
if (b.type != SlotType.OBJ && b.type != SlotType.UNKNOWN) return null
compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
}
else -> null
}
@ -994,29 +1101,54 @@ class BytecodeCompiler(
CompiledValue(out, SlotType.BOOL)
}
BinOp.BAND -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.AND_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
when (a.type) {
SlotType.INT -> {
builder.emit(Opcode.AND_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.OBJ, SlotType.UNKNOWN -> compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
else -> null
}
}
BinOp.BOR -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.OR_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
when (a.type) {
SlotType.INT -> {
builder.emit(Opcode.OR_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.OBJ, SlotType.UNKNOWN -> compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
else -> null
}
}
BinOp.BXOR -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.XOR_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
when (a.type) {
SlotType.INT -> {
builder.emit(Opcode.XOR_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.OBJ, SlotType.UNKNOWN -> compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
else -> null
}
}
BinOp.SHL -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.SHL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
when (a.type) {
SlotType.INT -> {
builder.emit(Opcode.SHL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.OBJ, SlotType.UNKNOWN -> compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
else -> null
}
}
BinOp.SHR -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.SHR_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
when (a.type) {
SlotType.INT -> {
builder.emit(Opcode.SHR_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.OBJ, SlotType.UNKNOWN -> compileObjBinaryOp(leftRef, a, b, op, refPos(ref))
else -> null
}
}
else -> null
}
@ -1472,7 +1604,7 @@ class BytecodeCompiler(
val target = ref.target
if (target is FieldRef) {
val receiverClass = resolveReceiverClass(target.target)
?: throw BytecodeFallbackException(
?: throw BytecodeCompileException(
"Member assignment requires compile-time receiver type: ${target.name}",
Pos.builtIn
)
@ -1485,7 +1617,7 @@ class BytecodeCompiler(
}
if (fieldId < 0 && methodId < 0) {
val extSlot = resolveExtensionSetterSlot(receiverClass, target.name)
?: throw BytecodeFallbackException(
?: throw BytecodeCompileException(
"Unknown member ${target.name} on ${receiverClass.className}",
Pos.builtIn
)
@ -1542,9 +1674,9 @@ class BytecodeCompiler(
val methodId = target.methodId ?: -1
if (fieldId < 0 && methodId < 0) {
val typeName = target.preferredThisTypeName()
?: throw BytecodeFallbackException("Missing member id for ${target.name}", Pos.builtIn)
?: throw BytecodeCompileException("Missing member id for ${target.name}", Pos.builtIn)
val wrapperName = extensionPropertySetterName(typeName, target.name)
val callee = resolveDirectNameSlot(wrapperName) ?: throw BytecodeFallbackException(
val callee = resolveDirectNameSlot(wrapperName) ?: throw BytecodeCompileException(
"Missing extension wrapper for ${typeName}.${target.name}",
Pos.builtIn
)
@ -1570,7 +1702,7 @@ class BytecodeCompiler(
val fieldId = target.fieldId() ?: -1
val methodId = target.methodId() ?: -1
if (fieldId < 0 && methodId < 0) {
throw BytecodeFallbackException("Missing member id for ${target.name}", Pos.builtIn)
throw BytecodeCompileException("Missing member id for ${target.name}", Pos.builtIn)
}
builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId, methodId, value.slot)
return value
@ -1580,7 +1712,7 @@ class BytecodeCompiler(
val fieldId = target.fieldId() ?: -1
val methodId = target.methodId() ?: -1
if (fieldId < 0 && methodId < 0) {
throw BytecodeFallbackException("Missing member id for ${target.name}", Pos.builtIn)
throw BytecodeCompileException("Missing member id for ${target.name}", Pos.builtIn)
}
builder.emit(Opcode.SET_MEMBER_SLOT, receiver.slot, fieldId, methodId, value.slot)
return value
@ -1673,7 +1805,7 @@ class BytecodeCompiler(
} ?: return compileEvalRef(ref)
val fieldTarget = ref.target as? FieldRef
if (fieldTarget != null) {
throw BytecodeFallbackException(
throw BytecodeCompileException(
"Member assignment requires compile-time receiver type: ${fieldTarget.name}",
Pos.builtIn
)
@ -1684,7 +1816,7 @@ class BytecodeCompiler(
val fieldId = implicitTarget.fieldId ?: -1
val methodId = implicitTarget.methodId ?: -1
if (fieldId < 0 && methodId < 0) {
throw BytecodeFallbackException("Missing member id for ${implicitTarget.name}", Pos.builtIn)
throw BytecodeCompileException("Missing member id for ${implicitTarget.name}", Pos.builtIn)
}
val current = allocSlot()
val result = allocSlot()
@ -1701,7 +1833,7 @@ class BytecodeCompiler(
val fieldId = thisFieldTarget.fieldId() ?: -1
val methodId = thisFieldTarget.methodId() ?: -1
if (fieldId < 0 && methodId < 0) {
throw BytecodeFallbackException("Missing member id for ${thisFieldTarget.name}", Pos.builtIn)
throw BytecodeCompileException("Missing member id for ${thisFieldTarget.name}", Pos.builtIn)
}
val current = allocSlot()
val result = allocSlot()
@ -1718,7 +1850,7 @@ class BytecodeCompiler(
val fieldId = qualifiedTarget.fieldId() ?: -1
val methodId = qualifiedTarget.methodId() ?: -1
if (fieldId < 0 && methodId < 0) {
throw BytecodeFallbackException("Missing member id for ${qualifiedTarget.name}", Pos.builtIn)
throw BytecodeCompileException("Missing member id for ${qualifiedTarget.name}", Pos.builtIn)
}
val current = allocSlot()
val result = allocSlot()
@ -1808,7 +1940,7 @@ class BytecodeCompiler(
}
is FieldRef -> {
val receiverClass = resolveReceiverClass(target.target)
?: throw BytecodeFallbackException(
?: throw BytecodeCompileException(
"Member assignment requires compile-time receiver type: ${target.name}",
Pos.builtIn
)
@ -1833,7 +1965,7 @@ class BytecodeCompiler(
}
} else {
val extSlot = resolveExtensionSetterSlot(receiverClass, target.name)
?: throw BytecodeFallbackException(
?: throw BytecodeCompileException(
"Unknown member ${target.name} on ${receiverClass.className}",
Pos.builtIn
)
@ -1895,7 +2027,7 @@ class BytecodeCompiler(
private fun compileFieldRef(ref: FieldRef): CompiledValue? {
val receiverClass = resolveReceiverClass(ref.target)
?: throw BytecodeFallbackException(
?: throw BytecodeCompileException(
"Member access requires compile-time receiver type: ${ref.name}",
Pos.builtIn
)
@ -1927,7 +2059,7 @@ class BytecodeCompiler(
return CompiledValue(dst, SlotType.OBJ)
}
val extSlot = resolveExtensionGetterSlot(receiverClass, ref.name)
?: throw BytecodeFallbackException(
?: throw BytecodeCompileException(
"Unknown member ${ref.name} on ${receiverClass.className}",
Pos.builtIn
)
@ -1964,7 +2096,7 @@ class BytecodeCompiler(
val fieldId = ref.fieldId() ?: -1
val methodId = ref.methodId() ?: -1
if (fieldId < 0 && methodId < 0) {
throw BytecodeFallbackException("Missing member id for ${ref.name}", Pos.builtIn)
throw BytecodeCompileException("Missing member id for ${ref.name}", Pos.builtIn)
}
val dst = allocSlot()
if (!ref.optional()) {
@ -1995,7 +2127,7 @@ class BytecodeCompiler(
val fieldId = ref.fieldId() ?: -1
val methodId = ref.methodId() ?: -1
if (fieldId < 0 && methodId < 0) {
throw BytecodeFallbackException("Missing member id for ${ref.name}", Pos.builtIn)
throw BytecodeCompileException("Missing member id for ${ref.name}", Pos.builtIn)
}
val dst = allocSlot()
if (!ref.optional()) {
@ -2296,7 +2428,7 @@ class BytecodeCompiler(
val fieldId = thisFieldTarget.fieldId() ?: -1
val methodId = thisFieldTarget.methodId() ?: -1
if (fieldId < 0 && methodId < 0) {
throw BytecodeFallbackException("Missing member id for ${thisFieldTarget.name}", Pos.builtIn)
throw BytecodeCompileException("Missing member id for ${thisFieldTarget.name}", Pos.builtIn)
}
val current = allocSlot()
builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, fieldId, methodId, current)
@ -2325,7 +2457,7 @@ class BytecodeCompiler(
val fieldId = implicitTarget.fieldId ?: -1
val methodId = implicitTarget.methodId ?: -1
if (fieldId < 0 && methodId < 0) {
throw BytecodeFallbackException("Missing member id for ${implicitTarget.name}", Pos.builtIn)
throw BytecodeCompileException("Missing member id for ${implicitTarget.name}", Pos.builtIn)
}
val current = allocSlot()
builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, fieldId, methodId, current)
@ -2354,7 +2486,7 @@ class BytecodeCompiler(
val fieldId = qualifiedTarget.fieldId() ?: -1
val methodId = qualifiedTarget.methodId() ?: -1
if (fieldId < 0 && methodId < 0) {
throw BytecodeFallbackException("Missing member id for ${qualifiedTarget.name}", Pos.builtIn)
throw BytecodeCompileException("Missing member id for ${qualifiedTarget.name}", Pos.builtIn)
}
val current = allocSlot()
builder.emit(Opcode.GET_MEMBER_SLOT, receiver.slot, fieldId, methodId, current)
@ -2381,7 +2513,7 @@ class BytecodeCompiler(
if (fieldTarget != null) {
if (fieldTarget.isOptional) return null
val receiverClass = resolveReceiverClass(fieldTarget.target)
?: throw BytecodeFallbackException(
?: throw BytecodeCompileException(
"Member access requires compile-time receiver type: ${fieldTarget.name}",
Pos.builtIn
)
@ -2591,7 +2723,7 @@ class BytecodeCompiler(
if (direct == null) {
val thisSlot = resolveDirectNameSlot("this")
if (thisSlot != null) {
throw BytecodeFallbackException(
throw BytecodeCompileException(
"Unresolved member call '${localTarget.name}': missing compile-time member id",
Pos.builtIn
)
@ -2600,7 +2732,7 @@ class BytecodeCompiler(
}
val fieldTarget = ref.target as? FieldRef
if (fieldTarget != null) {
throw BytecodeFallbackException(
throw BytecodeCompileException(
"Member call requires compile-time receiver type: ${fieldTarget.name}",
Pos.builtIn
)
@ -2684,10 +2816,17 @@ class BytecodeCompiler(
private fun compileMethodCall(ref: MethodCallRef): CompiledValue? {
val receiverClass = resolveReceiverClass(ref.receiver)
?: if (isAllowedObjectMember(ref.name)) {
Obj.rootObjectType
} else {
throw BytecodeCompileException(
"Member call requires compile-time receiver type: ${ref.name}",
Pos.builtIn
)
}
val receiver = compileRefWithFallback(ref.receiver, null, Pos.builtIn) ?: return null
val dst = allocSlot()
val methodId = receiverClass?.instanceMethodIdMap(includeAbstract = true)?.get(ref.name)
?: Obj.rootObjectType.instanceMethodIdMap(includeAbstract = true)[ref.name]
val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name]
if (methodId != null) {
if (!ref.isOptional) {
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
@ -2714,14 +2853,8 @@ class BytecodeCompiler(
builder.mark(endLabel)
return CompiledValue(dst, SlotType.OBJ)
}
if (receiverClass == null) {
throw BytecodeFallbackException(
"Member call requires compile-time receiver type: ${ref.name}",
Pos.builtIn
)
}
val extSlot = resolveExtensionCallableSlot(receiverClass, ref.name)
?: throw BytecodeFallbackException(
?: throw BytecodeCompileException(
"Unknown member ${ref.name} on ${receiverClass.className}",
Pos.builtIn
)
@ -2754,7 +2887,7 @@ class BytecodeCompiler(
private fun compileThisMethodSlotCall(ref: ThisMethodSlotCallRef): CompiledValue? {
val receiver = compileThisRef()
val methodId = ref.methodId() ?: throw BytecodeFallbackException(
val methodId = ref.methodId() ?: throw BytecodeCompileException(
"Missing member id for ${ref.methodName()}",
Pos.builtIn
)
@ -2810,7 +2943,7 @@ class BytecodeCompiler(
private fun compileQualifiedThisMethodSlotCall(ref: QualifiedThisMethodSlotCallRef): CompiledValue? {
val receiver = compileThisVariantRef(ref.receiverTypeName()) ?: return null
val methodId = ref.methodId() ?: throw BytecodeFallbackException(
val methodId = ref.methodId() ?: throw BytecodeCompileException(
"Missing member id for ${ref.methodName()}",
Pos.builtIn
)
@ -2925,8 +3058,8 @@ class BytecodeCompiler(
return when (stmt) {
is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos)
else -> {
throw BytecodeFallbackException(
"Bytecode fallback: unsupported argument expression",
throw BytecodeCompileException(
"Bytecode compile error: unsupported argument expression",
stmt.pos
)
}
@ -3081,15 +3214,15 @@ class BytecodeCompiler(
}
private fun emitFallbackStatement(stmt: Statement): CompiledValue {
throw BytecodeFallbackException(
"Bytecode fallback: unsupported statement",
throw BytecodeCompileException(
"Bytecode compile error: unsupported statement",
stmt.pos
)
}
private fun emitStatementEval(stmt: Statement): CompiledValue {
val stmtName = stmt::class.simpleName ?: "UnknownStatement"
throw BytecodeFallbackException("Unsupported statement in bytecode: $stmtName", stmt.pos)
throw BytecodeCompileException("Unsupported statement in bytecode: $stmtName", stmt.pos)
}
private fun compileStatementValueOrFallback(stmt: Statement, needResult: Boolean = true): CompiledValue? {
@ -3198,8 +3331,8 @@ class BytecodeCompiler(
val original = (statement as? BytecodeStatement)?.original
val name = original?.let { "${statement::class.simpleName}(${it::class.simpleName})" }
?: statement::class.simpleName
throw BytecodeFallbackException(
"Bytecode fallback: failed to compile block statement ($name)",
throw BytecodeCompileException(
"Bytecode compile error: failed to compile block statement ($name)",
statement.pos
)
}
@ -3531,16 +3664,16 @@ class BytecodeCompiler(
val iterableMethods = ObjIterable.instanceMethodIdMap(includeAbstract = true)
val iteratorMethodId = iterableMethods["iterator"]
if (iteratorMethodId == null) {
throw BytecodeFallbackException("Missing member id for Iterable.iterator", stmt.pos)
throw BytecodeCompileException("Missing member id for Iterable.iterator", stmt.pos)
}
val iteratorMethods = ObjIterator.instanceMethodIdMap(includeAbstract = true)
val hasNextMethodId = iteratorMethods["hasNext"]
if (hasNextMethodId == null) {
throw BytecodeFallbackException("Missing member id for Iterator.hasNext", stmt.pos)
throw BytecodeCompileException("Missing member id for Iterator.hasNext", stmt.pos)
}
val nextMethodId = iteratorMethods["next"]
if (nextMethodId == null) {
throw BytecodeFallbackException("Missing member id for Iterator.next", stmt.pos)
throw BytecodeCompileException("Missing member id for Iterator.next", stmt.pos)
}
val iterSlot = allocSlot()
@ -4020,8 +4153,8 @@ class BytecodeCompiler(
return when (target) {
is ExpressionStatement -> compileRefWithFallback(target.ref, SlotType.BOOL, target.pos)
else -> {
throw BytecodeFallbackException(
"Bytecode fallback: unsupported condition",
throw BytecodeCompileException(
"Bytecode compile error: unsupported condition",
pos
)
}
@ -4135,8 +4268,8 @@ class BytecodeCompiler(
val stack = loopStack.toList()
val targetIndex = findLoopContextIndex(stmt.label) ?: run {
val labels = stack.joinToString(prefix = "[", postfix = "]") { it.label ?: "<unlabeled>" }
throw BytecodeFallbackException(
"Bytecode fallback: break label '${stmt.label}' not found in $labels",
throw BytecodeCompileException(
"Bytecode compile error: break label '${stmt.label}' not found in $labels",
stmt.pos
)
}
@ -4329,7 +4462,7 @@ class BytecodeCompiler(
val localKeys = localSlotIndexByName.keys.sorted().joinToString(prefix = "[", postfix = "]")
val scopeKeys = scopeSlotIndexByName.keys.sorted().joinToString(prefix = "[", postfix = "]")
val info = " ref=$refKind loopSlots=$loopKeys localSlots=$localKeys scopeSlots=$scopeKeys forceScopeSlots=$forceScopeSlots"
throw BytecodeFallbackException("Unresolved name '$name'.$info", pos)
throw BytecodeCompileException("Unresolved name '$name'.$info", pos)
}
val refInfo = when (ref) {
is FieldRef -> "FieldRef(${ref.name})"
@ -4341,8 +4474,8 @@ class BytecodeCompiler(
} else {
""
}
throw BytecodeFallbackException(
"Bytecode fallback: unsupported expression ($refInfo)$extra",
throw BytecodeCompileException(
"Bytecode compile error: unsupported expression ($refInfo)$extra",
pos
)
}
@ -4354,6 +4487,7 @@ class BytecodeCompiler(
val fromSlot = slot?.let { slotObjClass[it] }
fromSlot
?: nameObjClass[ref.name]
?: resolveTypeNameClass(ref.name)
?: slotInitClassByKey[ScopeSlotKey(refScopeId(ref), refSlot(ref))]
?: run {
val match = slotInitClassByKey.entries.firstOrNull { (key, _) ->
@ -4365,6 +4499,7 @@ class BytecodeCompiler(
}
is LocalVarRef -> resolveDirectNameSlot(ref.name)?.let { slotObjClass[it.slot] }
?: nameObjClass[ref.name]
?: resolveTypeNameClass(ref.name)
is ListLiteralRef -> ObjList.type
is MapLiteralRef -> ObjMap.type
is RangeRef -> ObjRange.type
@ -4426,6 +4561,18 @@ class BytecodeCompiler(
}
}
private fun isAllowedObjectMember(memberName: String): Boolean {
return when (memberName) {
"toString",
"toInspectString",
"let",
"also",
"apply",
"run" -> true
else -> false
}
}
private fun refSlot(ref: LocalSlotRef): Int = ref.slot
private fun refScopeId(ref: LocalSlotRef): Int = ref.scopeId
private fun binaryLeft(ref: BinaryOpRef): ObjRef = ref.left
@ -4434,8 +4581,8 @@ class BytecodeCompiler(
private fun resolveReceiverClassForScopeCollection(ref: ObjRef): ObjClass? {
return when (ref) {
is LocalSlotRef -> nameObjClass[ref.name]
is LocalVarRef -> nameObjClass[ref.name]
is LocalSlotRef -> nameObjClass[ref.name] ?: resolveTypeNameClass(ref.name)
is LocalVarRef -> nameObjClass[ref.name] ?: resolveTypeNameClass(ref.name)
is ListLiteralRef -> ObjList.type
is MapLiteralRef -> ObjMap.type
is RangeRef -> ObjRange.type
@ -4605,6 +4752,9 @@ class BytecodeCompiler(
slotTypes.clear()
slotObjClass.clear()
nameObjClass.clear()
if (knownNameObjClass.isNotEmpty()) {
nameObjClass.putAll(knownNameObjClass)
}
slotInitClassByKey.clear()
scopeSlotMap.clear()
scopeSlotNameMap.clear()
@ -4623,6 +4773,13 @@ class BytecodeCompiler(
addrSlotByScopeSlot.clear()
loopStack.clear()
forceScopeSlots = allowLocalSlots && containsValueFnRef(stmt)
if (slotTypeByScopeId.isNotEmpty()) {
for ((scopeId, slots) in slotTypeByScopeId) {
for ((slotIndex, cls) in slots) {
slotInitClassByKey[ScopeSlotKey(scopeId, slotIndex)] = cls
}
}
}
if (allowLocalSlots) {
collectLoopVarNames(stmt)
}
@ -5144,7 +5301,6 @@ class BytecodeCompiler(
val receiverClass = resolveReceiverClassForScopeCollection(ref.receiver)
if (receiverClass != null) {
val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name]
?: Obj.rootObjectType.instanceMethodIdMap(includeAbstract = true)[ref.name]
if (methodId == null) {
queueExtensionCallableNames(receiverClass, ref.name)
}

View File

@ -27,6 +27,7 @@ import net.sergeych.lyng.WhenInCondition
import net.sergeych.lyng.WhenIsCondition
import net.sergeych.lyng.WhenStatement
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.RangeRef
class BytecodeStatement private constructor(
@ -50,13 +51,15 @@ class BytecodeStatement private constructor(
returnLabels: Set<String> = emptySet(),
rangeLocalNames: Set<String> = emptySet(),
allowedScopeNames: Set<String>? = null,
slotTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
knownNameObjClass: Map<String, ObjClass> = emptyMap(),
): Statement {
if (statement is BytecodeStatement) return statement
val hasUnsupported = containsUnsupportedStatement(statement)
if (hasUnsupported) {
val statementName = statement::class.qualifiedName ?: statement::class.simpleName ?: "UnknownStatement"
throw BytecodeFallbackException(
"Bytecode fallback: unsupported statement $statementName in '$nameHint'",
throw BytecodeCompileException(
"Bytecode compile error: unsupported statement $statementName in '$nameHint'",
statement.pos
)
}
@ -65,11 +68,13 @@ class BytecodeStatement private constructor(
allowLocalSlots = safeLocals,
returnLabels = returnLabels,
rangeLocalNames = rangeLocalNames,
allowedScopeNames = allowedScopeNames
allowedScopeNames = allowedScopeNames,
slotTypeByScopeId = slotTypeByScopeId,
knownNameObjClass = knownNameObjClass
)
val compiled = compiler.compileStatement(nameHint, statement)
val fn = compiled ?: throw BytecodeFallbackException(
"Bytecode fallback: failed to compile '$nameHint'",
val fn = compiled ?: throw BytecodeCompileException(
"Bytecode compile error: failed to compile '$nameHint'",
statement.pos
)
return BytecodeStatement(statement, fn)

View File

@ -30,7 +30,6 @@ class CmdBuilder {
private val constPool = mutableListOf<BytecodeConst>()
private val labelPositions = mutableMapOf<Label, Int>()
private var nextLabelId = 0
private val fallbackStatements = mutableListOf<net.sergeych.lyng.Statement>()
fun addConst(c: BytecodeConst): Int {
constPool += c
@ -51,11 +50,6 @@ class CmdBuilder {
labelPositions[label] = instructions.size
}
fun addFallback(stmt: net.sergeych.lyng.Statement): Int {
fallbackStatements += stmt
return fallbackStatements.lastIndex
}
fun build(
name: String,
localCount: Int,
@ -109,7 +103,6 @@ class CmdBuilder {
localSlotNames = localSlotNames,
localSlotMutables = localSlotMutables,
constants = constPool.toList(),
fallbackStatements = fallbackStatements.toList(),
cmds = cmds.toTypedArray()
)
}
@ -175,12 +168,6 @@ class CmdBuilder {
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_SLOT ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_VIRTUAL ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.GET_FIELD ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT)
Opcode.SET_FIELD ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT)
Opcode.GET_INDEX ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.SET_INDEX ->
@ -382,10 +369,7 @@ class CmdBuilder {
Opcode.DECL_EXT_PROPERTY -> CmdDeclExtProperty(operands[0], operands[1])
Opcode.CALL_DIRECT -> CmdCallDirect(operands[0], operands[1], operands[2], operands[3])
Opcode.CALL_MEMBER_SLOT -> CmdCallMemberSlot(operands[0], operands[1], operands[2], operands[3], operands[4])
Opcode.CALL_VIRTUAL -> CmdCallVirtual(operands[0], operands[1], operands[2], operands[3], operands[4])
Opcode.CALL_SLOT -> CmdCallSlot(operands[0], operands[1], operands[2], operands[3])
Opcode.GET_FIELD -> CmdGetField(operands[0], operands[1], operands[2])
Opcode.SET_FIELD -> CmdSetField(operands[0], operands[1], operands[2])
Opcode.GET_INDEX -> CmdGetIndex(operands[0], operands[1], operands[2])
Opcode.SET_INDEX -> CmdSetIndex(operands[0], operands[1], operands[2])
Opcode.LIST_LITERAL -> CmdListLiteral(operands[0], operands[1], operands[2], operands[3])

View File

@ -1,21 +0,0 @@
/*
* 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 CmdCallSiteCache {
fun methodCallSites(fn: CmdFunction): MutableMap<Int, MethodCallSite>
}

View File

@ -180,11 +180,8 @@ object CmdDisassembler {
is CmdDeclLocal -> Opcode.DECL_LOCAL to intArrayOf(cmd.constId, cmd.slot)
is CmdDeclExtProperty -> Opcode.DECL_EXT_PROPERTY to intArrayOf(cmd.constId, cmd.slot)
is CmdCallDirect -> Opcode.CALL_DIRECT to intArrayOf(cmd.id, cmd.argBase, cmd.argCount, cmd.dst)
is CmdCallVirtual -> Opcode.CALL_VIRTUAL to intArrayOf(cmd.recvSlot, cmd.methodId, cmd.argBase, cmd.argCount, cmd.dst)
is CmdCallMemberSlot -> Opcode.CALL_MEMBER_SLOT to intArrayOf(cmd.recvSlot, cmd.methodId, cmd.argBase, cmd.argCount, cmd.dst)
is CmdCallSlot -> Opcode.CALL_SLOT to intArrayOf(cmd.calleeSlot, cmd.argBase, cmd.argCount, cmd.dst)
is CmdGetField -> Opcode.GET_FIELD to intArrayOf(cmd.recvSlot, cmd.fieldId, cmd.dst)
is CmdSetField -> Opcode.SET_FIELD to intArrayOf(cmd.recvSlot, cmd.fieldId, cmd.valueSlot)
is CmdGetIndex -> Opcode.GET_INDEX to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.dst)
is CmdSetIndex -> Opcode.SET_INDEX to intArrayOf(cmd.targetSlot, cmd.indexSlot, cmd.valueSlot)
is CmdListLiteral -> Opcode.LIST_LITERAL to intArrayOf(cmd.planId, cmd.baseSlot, cmd.count, cmd.dst)
@ -266,14 +263,8 @@ object CmdDisassembler {
listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_SLOT ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_VIRTUAL ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_MEMBER_SLOT ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.GET_FIELD ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT)
Opcode.SET_FIELD ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT)
Opcode.GET_INDEX ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.SET_INDEX ->

View File

@ -28,7 +28,6 @@ data class CmdFunction(
val localSlotNames: Array<String?>,
val localSlotMutables: BooleanArray,
val constants: List<BytecodeConst>,
val fallbackStatements: List<net.sergeych.lyng.Statement>,
val cmds: Array<Cmd>,
) {
init {

View File

@ -17,10 +17,8 @@
package net.sergeych.lyng.bytecode
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.ExecutionError
import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.PerfStats
import net.sergeych.lyng.Pos
import net.sergeych.lyng.ReturnException
import net.sergeych.lyng.Scope
@ -1160,44 +1158,6 @@ class CmdCallDirect(
}
}
class CmdCallVirtual(
internal val recvSlot: Int,
internal val methodId: Int,
internal val argBase: Int,
internal val argCount: Int,
internal val dst: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
frame.scope.raiseError("CALL_VIRTUAL is not allowed: compile-time member resolution is required")
}
}
class CmdCallFallback(
internal val id: Int,
internal val argBase: Int,
internal val argCount: Int,
internal val dst: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncFrameToScope()
}
val stmt = frame.fn.fallbackStatements.getOrNull(id)
?: error("Fallback statement not found: $id")
val args = frame.buildArguments(argBase, argCount)
val result = if (PerfFlags.SCOPE_POOL) {
frame.scope.withChildFrame(args) { child -> stmt.execute(child) }
} else {
stmt.execute(frame.scope.createChildScope(frame.scope.pos, args = args))
}
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncScopeToFrame()
}
frame.storeObjResult(dst, result)
return
}
}
class CmdCallSlot(
internal val calleeSlot: Int,
internal val argBase: Int,
@ -1236,63 +1196,6 @@ class CmdCallSlot(
}
}
class CmdGetField(
internal val recvSlot: Int,
internal val fieldId: Int,
internal val dst: Int,
) : Cmd() {
private var rKey: Long = 0L
private var rVer: Int = -1
override suspend fun perform(frame: CmdFrame) {
val receiver = frame.slotToObj(recvSlot)
val nameConst = frame.fn.constants.getOrNull(fieldId) as? BytecodeConst.StringVal
?: error("GET_FIELD expects StringVal at $fieldId")
if (PerfFlags.FIELD_PIC) {
val (key, ver) = when (receiver) {
is ObjInstance -> receiver.objClass.classId to receiver.objClass.layoutVersion
is ObjClass -> receiver.classId to receiver.layoutVersion
else -> 0L to -1
}
if (key != 0L) {
if (key == rKey && ver == rVer) {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.fieldPicHit++
} else {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.fieldPicMiss++
rKey = key
rVer = ver
}
}
}
val result = receiver.readField(frame.scope, nameConst.value).value
frame.storeObjResult(dst, result)
return
}
}
class CmdGetName(
internal val nameId: Int,
internal val dst: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncFrameToScope()
}
val nameConst = frame.fn.constants.getOrNull(nameId) as? BytecodeConst.StringVal
?: error("GET_NAME expects StringVal at $nameId")
val name = nameConst.value
val result = frame.scope.get(name)?.value ?: run {
try {
frame.scope.thisObj.readField(frame.scope, name).value
} catch (e: ExecutionError) {
if ((e.message ?: "").contains("no such field: $name")) ObjUnset else throw e
}
}
frame.storeObjResult(dst, result)
return
}
}
class CmdListLiteral(
internal val planId: Int,
internal val baseSlot: Int,
@ -1407,39 +1310,6 @@ class CmdCallMemberSlot(
}
}
class CmdSetField(
internal val recvSlot: Int,
internal val fieldId: Int,
internal val valueSlot: Int,
) : Cmd() {
private var wKey: Long = 0L
private var wVer: Int = -1
override suspend fun perform(frame: CmdFrame) {
val receiver = frame.slotToObj(recvSlot)
val nameConst = frame.fn.constants.getOrNull(fieldId) as? BytecodeConst.StringVal
?: error("SET_FIELD expects StringVal at $fieldId")
if (PerfFlags.FIELD_PIC) {
val (key, ver) = when (receiver) {
is ObjInstance -> receiver.objClass.classId to receiver.objClass.layoutVersion
is ObjClass -> receiver.classId to receiver.layoutVersion
else -> 0L to -1
}
if (key != 0L) {
if (key == wKey && ver == wVer) {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.fieldPicSetHit++
} else {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.fieldPicSetMiss++
wKey = key
wVer = ver
}
}
}
receiver.writeField(frame.scope, nameConst.value, frame.slotToObj(valueSlot))
return
}
}
class CmdGetIndex(
internal val targetSlot: Int,
internal val indexSlot: Int,
@ -1463,18 +1333,6 @@ class CmdSetIndex(
}
}
class CmdEvalFallback(internal val id: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val stmt = frame.fn.fallbackStatements.getOrNull(id)
?: error("Fallback statement not found: $id")
frame.syncFrameToScope()
val result = stmt.execute(frame.scope)
frame.syncScopeToFrame()
frame.storeObjResult(dst, result)
return
}
}
class CmdEvalRef(internal val id: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
if (frame.fn.localSlotNames.isNotEmpty()) {
@ -1558,7 +1416,6 @@ class CmdFrame(
var ip: Int = 0
var scope: Scope = scope0
private val moduleScope: Scope = resolveModuleScope(scope0)
val methodCallSites: MutableMap<Int, MethodCallSite> = CmdCallSiteCache.methodCallSites(fn)
internal val scopeStack = ArrayDeque<Scope>()
internal val scopeVirtualStack = ArrayDeque<Boolean>()

View File

@ -1,241 +0,0 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.bytecode
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.ExecutionError
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.PerfStats
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Visibility
import net.sergeych.lyng.canAccessMember
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjIllegalAccessException
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjProperty
import net.sergeych.lyng.obj.ObjRecord
class MethodCallSite(private val name: String) {
private var mKey1: Long = 0L; private var mVer1: Int = -1
private var mInvoker1: (suspend (Obj, Scope, Arguments) -> Obj)? = null
private var mKey2: Long = 0L; private var mVer2: Int = -1
private var mInvoker2: (suspend (Obj, Scope, Arguments) -> Obj)? = null
private var mKey3: Long = 0L; private var mVer3: Int = -1
private var mInvoker3: (suspend (Obj, Scope, Arguments) -> Obj)? = null
private var mKey4: Long = 0L; private var mVer4: Int = -1
private var mInvoker4: (suspend (Obj, Scope, Arguments) -> Obj)? = null
private var mAccesses: Int = 0; private var mMisses: Int = 0; private var mPromotedTo4: Boolean = false
private var mFreezeWindowsLeft: Int = 0
private var mWindowAccesses: Int = 0
private var mWindowMisses: Int = 0
private inline fun size4MethodsEnabled(): Boolean =
PerfFlags.METHOD_PIC_SIZE_4 ||
((PerfFlags.PIC_ADAPTIVE_2_TO_4 || PerfFlags.PIC_ADAPTIVE_METHODS_ONLY) && mPromotedTo4 && mFreezeWindowsLeft == 0)
private fun noteMethodHit() {
if (!(PerfFlags.PIC_ADAPTIVE_2_TO_4 || PerfFlags.PIC_ADAPTIVE_METHODS_ONLY)) return
val a = (mAccesses + 1).coerceAtMost(1_000_000)
mAccesses = a
if (PerfFlags.PIC_ADAPTIVE_HEURISTIC) {
mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000)
if (mWindowAccesses >= 256) endHeuristicWindow()
}
}
private fun noteMethodMiss() {
if (!(PerfFlags.PIC_ADAPTIVE_2_TO_4 || PerfFlags.PIC_ADAPTIVE_METHODS_ONLY)) return
val a = (mAccesses + 1).coerceAtMost(1_000_000)
mAccesses = a
mMisses = (mMisses + 1).coerceAtMost(1_000_000)
if (!mPromotedTo4 && mFreezeWindowsLeft == 0 && a >= 256) {
if (mMisses * 100 / a > 20) mPromotedTo4 = true
mAccesses = 0; mMisses = 0
}
if (PerfFlags.PIC_ADAPTIVE_HEURISTIC) {
mWindowAccesses = (mWindowAccesses + 1).coerceAtMost(1_000_000)
mWindowMisses = (mWindowMisses + 1).coerceAtMost(1_000_000)
if (mWindowAccesses >= 256) endHeuristicWindow()
}
}
private fun endHeuristicWindow() {
val accesses = mWindowAccesses
val misses = mWindowMisses
mWindowAccesses = 0
mWindowMisses = 0
if (mFreezeWindowsLeft > 0) {
mFreezeWindowsLeft = (mFreezeWindowsLeft - 1).coerceAtLeast(0)
return
}
if (mPromotedTo4 && accesses >= 256) {
val rate = misses * 100 / accesses
if (rate >= 25) {
mPromotedTo4 = false
mFreezeWindowsLeft = 4
}
}
}
suspend fun invoke(scope: Scope, base: Obj, callArgs: Arguments): Obj {
if (PerfFlags.METHOD_PIC) {
val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
is ObjClass -> base.classId to base.layoutVersion
else -> 0L to -1
}
if (key != 0L) {
mInvoker1?.let { inv ->
if (key == mKey1 && ver == mVer1) {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.methodPicHit++
noteMethodHit()
return inv(base, scope, callArgs)
}
}
mInvoker2?.let { inv ->
if (key == mKey2 && ver == mVer2) {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.methodPicHit++
noteMethodHit()
val tK = mKey2; val tV = mVer2; val tI = mInvoker2
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
mKey1 = tK; mVer1 = tV; mInvoker1 = tI
return inv(base, scope, callArgs)
}
}
if (size4MethodsEnabled()) mInvoker3?.let { inv ->
if (key == mKey3 && ver == mVer3) {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.methodPicHit++
noteMethodHit()
val tK = mKey3; val tV = mVer3; val tI = mInvoker3
mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
mKey1 = tK; mVer1 = tV; mInvoker1 = tI
return inv(base, scope, callArgs)
}
}
if (size4MethodsEnabled()) mInvoker4?.let { inv ->
if (key == mKey4 && ver == mVer4) {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.methodPicHit++
noteMethodHit()
val tK = mKey4; val tV = mVer4; val tI = mInvoker4
mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3
mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
mKey1 = tK; mVer1 = tV; mInvoker1 = tI
return inv(base, scope, callArgs)
}
}
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.methodPicMiss++
noteMethodMiss()
val result = try {
base.invokeInstanceMethod(scope, name, callArgs)
} catch (e: ExecutionError) {
mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3
mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
mKey1 = key; mVer1 = ver; mInvoker1 = { _, sc, _ ->
sc.raiseError(e.message ?: "method not found: $name")
}
throw e
}
if (size4MethodsEnabled()) {
mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3
mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2
}
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
when (base) {
is ObjInstance -> {
val cls0 = base.objClass
val keyInScope = cls0.publicMemberResolution[name]
val methodSlot = if (keyInScope != null) cls0.methodSlotForKey(keyInScope) else null
val fastRec = if (methodSlot != null) {
val idx = methodSlot.slot
if (idx >= 0 && idx < base.methodSlots.size) base.methodSlots[idx] else null
} else if (keyInScope != null) {
base.methodRecordForKey(keyInScope) ?: base.instanceScope.objects[keyInScope]
} else null
val resolved = if (fastRec != null) null else cls0.resolveInstanceMember(name)
val targetRec = when {
fastRec != null && fastRec.type == ObjRecord.Type.Fun -> fastRec
resolved != null && resolved.record.type == ObjRecord.Type.Fun && !resolved.record.isAbstract -> resolved.record
else -> null
}
if (targetRec != null) {
val visibility = targetRec.visibility
val decl = targetRec.declaringClass ?: (resolved?.declaringClass ?: cls0)
if (methodSlot != null && targetRec.type == ObjRecord.Type.Fun) {
val slotIndex = methodSlot.slot
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
val inst = obj as ObjInstance
if (inst.objClass === cls0) {
val rec = if (slotIndex >= 0 && slotIndex < inst.methodSlots.size) inst.methodSlots[slotIndex] else null
if (rec != null && rec.type == ObjRecord.Type.Fun && !rec.isAbstract) {
if (!visibility.isPublic && !canAccessMember(visibility, decl, sc.currentClassCtx, name)) {
sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name"))
}
rec.value.invoke(inst.instanceScope, inst, a, decl)
} else {
obj.invokeInstanceMethod(sc, name, a)
}
} else {
obj.invokeInstanceMethod(sc, name, a)
}
}
} else {
val callable = targetRec.value
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
val inst = obj as ObjInstance
if (!visibility.isPublic && !canAccessMember(visibility, decl, sc.currentClassCtx, name)) {
sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name"))
}
callable.invoke(inst.instanceScope, inst, a)
}
}
} else {
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
obj.invokeInstanceMethod(sc, name, a)
}
}
}
is ObjClass -> {
val clsScope = base.classScope
val rec = clsScope?.get(name)
if (rec != null) {
val callable = rec.value
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
callable.invoke(sc, obj, a)
}
} else {
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
obj.invokeInstanceMethod(sc, name, a)
}
}
}
else -> {
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
obj.invokeInstanceMethod(sc, name, a)
}
}
}
return result
}
}
return base.invokeInstanceMethod(scope, name, callArgs)
}
}

View File

@ -126,12 +126,9 @@ enum class Opcode(val code: Int) {
DECL_EXT_PROPERTY(0x8A),
CALL_DIRECT(0x90),
CALL_VIRTUAL(0x91),
CALL_MEMBER_SLOT(0x92),
CALL_SLOT(0x93),
GET_FIELD(0xA0),
SET_FIELD(0xA1),
GET_INDEX(0xA2),
SET_INDEX(0xA3),
LIST_LITERAL(0xA5),

View File

@ -1,25 +0,0 @@
/*
* 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 CmdCallSiteCache {
private val cache = mutableMapOf<CmdFunction, MutableMap<Int, MethodCallSite>>()
actual fun methodCallSites(fn: CmdFunction): MutableMap<Int, MethodCallSite> {
return cache.getOrPut(fn) { mutableMapOf() }
}
}

View File

@ -1,30 +0,0 @@
/*
* 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 CmdCallSiteCache {
private val cache = ThreadLocal.withInitial {
IdentityHashMap<CmdFunction, MutableMap<Int, MethodCallSite>>()
}
actual fun methodCallSites(fn: CmdFunction): MutableMap<Int, MethodCallSite> {
val map = cache.get()
return map.getOrPut(fn) { mutableMapOf() }
}
}

View File

@ -1,26 +0,0 @@
/*
* 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 CmdCallSiteCache {
private val cache = mutableMapOf<CmdFunction, MutableMap<Int, MethodCallSite>>()
actual fun methodCallSites(fn: CmdFunction): MutableMap<Int, MethodCallSite> {
return cache.getOrPut(fn) { mutableMapOf() }
}
}

View File

@ -1,25 +0,0 @@
/*
* 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 CmdCallSiteCache {
private val cache = mutableMapOf<CmdFunction, MutableMap<Int, MethodCallSite>>()
actual fun methodCallSites(fn: CmdFunction): MutableMap<Int, MethodCallSite> {
return cache.getOrPut(fn) { mutableMapOf() }
}
}

View File

@ -291,7 +291,7 @@ override fun List<T>.toString() {
var result = "["
for (item in this) {
if (!first) result += ","
result += item.toString()
result += (item as Object).toString()
first = false
}
result + "]"
@ -310,8 +310,9 @@ fun List<T>.sort(): Void {
/* Print this exception and its stack trace to standard output. */
fun Exception.printStackTrace(): Void {
println(this)
for( entry in stackTrace )
println("\tat "+entry.toString())
for( entry in stackTrace ) {
println("\tat "+(entry as Object).toString())
}
}
/* Compile this string into a regular expression. */
@ -371,7 +372,7 @@ class lazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,T
private var value = Unset
override fun bind(name: String, access: DelegateAccess, thisRef: ThisRefType): Object {
if (access.toString() != "DelegateAccess.Val") throw "lazy delegate can only be used with 'val'"
if (access != DelegateAccess.Val) throw "lazy delegate can only be used with 'val'"
this
}

View File

@ -242,6 +242,7 @@ This metadata drives:
## Migration Notes
- Keep reflection APIs separate to audit usage.
- Add warnings for member shadowing to surface risky code.
- Runtime fallback opcodes are removed (CALL_VIRTUAL/GET_FIELD/SET_FIELD); unresolved names or members are compile-time errors.
## Compatibility Notes (Kotlin interop)
- Provide minimal Kotlin-facing APIs that mirror compile-time-visible names.