optimization: Scope access is now a Kotlin interface, compiler uses direct slot access also for closures

This commit is contained in:
Sergey Chernov 2026-02-05 13:55:24 +03:00
parent 43a6a7aaf4
commit 6220e982a0
14 changed files with 438 additions and 165 deletions

View File

@ -12,3 +12,9 @@
- `void` is a singleton of class `Void` (syntax sugar for return type).
- Object members are always allowed even on unknown types; non-Object members require explicit casts. Remove `inspect` from Object and use `toInspectString()` instead.
- Do not reintroduce bytecode fallback opcodes (e.g., `GET_NAME`, `EVAL_*`, `CALL_FALLBACK`) or runtime name-resolution fallbacks; all symbol resolution must stay compile-time only.
## Bytecode frame-first migration plan
- Treat frame slots as the only storage for locals/temps by default; avoid pre-creating scope slot mappings for compiled functions.
- Create closure references only when a capture is detected; use a direct frame+slot reference (foreign slot ref) instead of scope slots.
- Keep Scope as a lazy reflection facade: resolve name -> slot only on demand for Kotlin interop (no eager name mapping on every call).
- Avoid PUSH_SCOPE/POP_SCOPE in bytecode for loops/functions unless dynamic name access or Kotlin reflection is requested.

View File

@ -4,15 +4,21 @@
test the Lyng way. It is not meant to be effective.
*/
fun naiveCountHappyNumbers() {
fun naiveCountHappyNumbers(): Int {
var count = 0
for( n1 in 0..9 )
for( n2 in 0..9 )
for( n3 in 0..9 )
for( n4 in 0..9 )
for( n5 in 0..9 )
for( n6 in 0..9 )
for( n1 in 0..9 ) {
for( n2 in 0..9 ) {
for( n3 in 0..9 ) {
for( n4 in 0..9 ) {
for( n5 in 0..9 ) {
for( n6 in 0..9 ) {
if( n1 + n2 + n3 == n4 + n5 + n6 ) count++
}
}
}
}
}
}
count
}
@ -28,4 +34,3 @@ for( r in 1..900 ) {
assert( found == 55252 )
delay(0.05)
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -56,7 +56,7 @@ class FsIntegrationJvmTest {
"""
import lyng.io.fs
// list current folder files
println( Path(".").list().toList() )
println( Path(".").list() )
""".trimIndent()
)
}

View File

@ -1131,6 +1131,7 @@ class Compiler(
"<script>",
allowLocalSlots = true,
allowedScopeNames = modulePlan.keys,
moduleScopeId = moduleSlotPlan()?.id,
slotTypeByScopeId = slotTypeByScopeId,
knownNameObjClass = knownClassMapForBytecode()
)
@ -1357,7 +1358,7 @@ class Compiler(
if (scope == null) return
for ((name, rec) in scope.objects) {
val cls = rec.value as? ObjClass ?: continue
result.putIfAbsent(name, cls)
if (!result.containsKey(name)) result[name] = cls
}
}
addScope(seedScope)
@ -1367,7 +1368,7 @@ class Compiler(
}
for (name in compileClassInfos.keys) {
val cls = resolveClassByName(name) ?: continue
result.putIfAbsent(name, cls)
if (!result.containsKey(name)) result[name] = cls
}
return result
}
@ -1411,6 +1412,7 @@ class Compiler(
returnLabels = returnLabels,
rangeLocalNames = currentRangeParamNames,
allowedScopeNames = allowedScopeNames,
moduleScopeId = moduleSlotPlan()?.id,
slotTypeByScopeId = slotTypeByScopeId,
knownNameObjClass = knownClassMapForBytecode()
)
@ -1441,6 +1443,7 @@ class Compiler(
returnLabels = returnLabels,
rangeLocalNames = currentRangeParamNames,
allowedScopeNames = allowedScopeNames,
moduleScopeId = moduleSlotPlan()?.id,
slotTypeByScopeId = slotTypeByScopeId,
knownNameObjClass = knownNames
)

View File

@ -0,0 +1,57 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* 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
import net.sergeych.lyng.bytecode.SlotType
import net.sergeych.lyng.obj.*
interface FrameAccess {
fun getSlotTypeCode(slot: Int): Byte
fun getObj(slot: Int): Obj
fun getInt(slot: Int): Long
fun getReal(slot: Int): Double
fun getBool(slot: Int): Boolean
fun setObj(slot: Int, value: Obj)
fun setInt(slot: Int, value: Long)
fun setReal(slot: Int, value: Double)
fun setBool(slot: Int, value: Boolean)
}
class FrameSlotRef(
private val frame: FrameAccess,
private val slot: Int,
) : net.sergeych.lyng.obj.Obj() {
fun read(): Obj {
return when (frame.getSlotTypeCode(slot)) {
SlotType.INT.code -> ObjInt.of(frame.getInt(slot))
SlotType.REAL.code -> ObjReal.of(frame.getReal(slot))
SlotType.BOOL.code -> if (frame.getBool(slot)) ObjTrue else ObjFalse
SlotType.OBJ.code -> frame.getObj(slot)
else -> ObjNull
}
}
fun write(value: Obj) {
when (value) {
is ObjInt -> frame.setInt(slot, value.value)
is ObjReal -> frame.setReal(slot, value.value)
is ObjBool -> frame.setBool(slot, value.value)
else -> frame.setObj(slot, value)
}
}
}

View File

@ -17,9 +17,9 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.bytecode.CmdDisassembler
import net.sergeych.lyng.bytecode.BytecodeStatement
import net.sergeych.lyng.bytecode.CmdDisassembler
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.pacman.ImportProvider
@ -414,7 +414,13 @@ open class Scope(
// Slot fast-path API
fun getSlotRecord(index: Int): ObjRecord = slots[index]
fun setSlotValue(index: Int, newValue: Obj) {
slots[index].value = newValue
val record = slots[index]
val value = record.value
if (value is FrameSlotRef) {
value.write(newValue)
return
}
record.value = newValue
}
val slotCount: Int
get() = slots.size
@ -839,11 +845,21 @@ open class Scope(
}
suspend fun resolve(rec: ObjRecord, name: String): Obj {
val value = rec.value
if (value is FrameSlotRef) {
return value.read()
}
val receiver = rec.receiver ?: thisObj
return receiver.resolveRecord(this, rec, name, rec.declaringClass).value
}
suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) {
val value = rec.value
if (value is FrameSlotRef) {
if (!rec.isMutable && value.read() !== ObjUnset) raiseIllegalAssignment("can't reassign val $name")
value.write(newValue)
return
}
if (rec.type == ObjRecord.Type.Delegated) {
val receiver = rec.receiver ?: thisObj
val del = rec.delegate ?: run {

View File

@ -25,6 +25,7 @@ class BytecodeCompiler(
private val returnLabels: Set<String> = emptySet(),
private val rangeLocalNames: Set<String> = emptySet(),
private val allowedScopeNames: Set<String>? = null,
private val moduleScopeId: Int? = null,
private val slotTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
private val knownNameObjClass: Map<String, ObjClass> = emptyMap(),
) {
@ -58,6 +59,7 @@ class BytecodeCompiler(
private val intLoopVarNames = LinkedHashSet<String>()
private val loopStack = ArrayDeque<LoopContext>()
private var forceScopeSlots = false
private var currentPos: Pos? = null
private data class LoopContext(
val label: String?,
@ -70,6 +72,7 @@ class BytecodeCompiler(
fun compileStatement(name: String, stmt: net.sergeych.lyng.Statement): CmdFunction? {
prepareCompilation(stmt)
setPos(stmt.pos)
return when (stmt) {
is ExpressionStatement -> compileExpression(name, stmt)
is net.sergeych.lyng.IfStatement -> compileIf(name, stmt)
@ -321,6 +324,7 @@ class BytecodeCompiler(
}
private fun compileImplicitThisMethodCall(ref: ImplicitThisMethodCallRef): CompiledValue? {
val callPos = ref.pos()
val receiver = ref.preferredThisTypeName()?.let { typeName ->
compileThisVariantRef(typeName) ?: return null
} ?: compileThisRef()
@ -330,6 +334,7 @@ class BytecodeCompiler(
if (!ref.optionalInvoke()) {
val args = compileCallArgs(ref.arguments(), ref.hasTailBlock()) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, methodId, args.base, encodedCount, dst)
return CompiledValue(dst, SlotType.OBJ)
}
@ -345,6 +350,7 @@ class BytecodeCompiler(
)
val args = compileCallArgs(ref.arguments(), ref.hasTailBlock()) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, methodId, args.base, encodedCount, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
@ -365,6 +371,7 @@ class BytecodeCompiler(
if (!ref.optionalInvoke()) {
val args = compileCallArgsWithReceiver(receiver, ref.arguments(), ref.hasTailBlock()) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_SLOT, calleeObj.slot, args.base, encodedCount, dst)
return CompiledValue(dst, SlotType.OBJ)
}
@ -380,6 +387,7 @@ class BytecodeCompiler(
)
val args = compileCallArgsWithReceiver(receiver, ref.arguments(), ref.hasTailBlock()) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_SLOT, calleeObj.slot, args.base, encodedCount, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
@ -2707,6 +2715,7 @@ class BytecodeCompiler(
}
private fun compileCall(ref: CallRef): CompiledValue? {
val callPos = callSitePos()
val localTarget = ref.target as? LocalVarRef
if (localTarget != null) {
val direct = resolveDirectNameSlot(localTarget.name)
@ -2732,11 +2741,12 @@ class BytecodeCompiler(
"Map" -> ObjMap.type
else -> null
}
val callee = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null
val callee = compileRefWithFallback(ref.target, null, refPosOrCurrent(ref.target)) ?: return null
val dst = allocSlot()
if (!ref.isOptionalInvoke) {
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, encodedCount, dst)
if (initClass != null) {
slotObjClass[dst] = initClass
@ -2755,6 +2765,7 @@ class BytecodeCompiler(
)
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, encodedCount, dst)
if (initClass != null) {
slotObjClass[dst] = initClass
@ -2805,6 +2816,7 @@ class BytecodeCompiler(
}
private fun compileMethodCall(ref: MethodCallRef): CompiledValue? {
val callPos = callSitePos()
val receiverClass = resolveReceiverClass(ref.receiver)
?: if (isAllowedObjectMember(ref.name)) {
Obj.rootObjectType
@ -2814,13 +2826,14 @@ class BytecodeCompiler(
Pos.builtIn
)
}
val receiver = compileRefWithFallback(ref.receiver, null, Pos.builtIn) ?: return null
val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null
val dst = allocSlot()
val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name]
if (methodId != null) {
if (!ref.isOptional) {
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, methodId, args.base, encodedCount, dst)
return CompiledValue(dst, SlotType.OBJ)
}
@ -2836,6 +2849,7 @@ class BytecodeCompiler(
)
val args = compileCallArgs(ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, methodId, args.base, encodedCount, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
@ -2852,6 +2866,7 @@ class BytecodeCompiler(
if (!ref.isOptional) {
val args = compileCallArgsWithReceiver(receiver, ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, encodedCount, dst)
return CompiledValue(dst, SlotType.OBJ)
}
@ -2867,6 +2882,7 @@ class BytecodeCompiler(
)
val args = compileCallArgsWithReceiver(receiver, ref.args, ref.tailBlock) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, encodedCount, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
@ -2876,6 +2892,7 @@ class BytecodeCompiler(
}
private fun compileThisMethodSlotCall(ref: ThisMethodSlotCallRef): CompiledValue? {
val callPos = callSitePos()
val receiver = compileThisRef()
val methodId = ref.methodId() ?: throw BytecodeCompileException(
"Missing member id for ${ref.methodName()}",
@ -2885,6 +2902,7 @@ class BytecodeCompiler(
if (!ref.optionalInvoke()) {
val args = compileCallArgs(ref.arguments(), ref.hasTailBlock()) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, methodId, args.base, encodedCount, dst)
return CompiledValue(dst, SlotType.OBJ)
}
@ -2900,6 +2918,7 @@ class BytecodeCompiler(
)
val args = compileCallArgs(ref.arguments(), ref.hasTailBlock()) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, methodId, args.base, encodedCount, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
@ -2932,6 +2951,7 @@ class BytecodeCompiler(
}
private fun compileQualifiedThisMethodSlotCall(ref: QualifiedThisMethodSlotCallRef): CompiledValue? {
val callPos = callSitePos()
val receiver = compileThisVariantRef(ref.receiverTypeName()) ?: return null
val methodId = ref.methodId() ?: throw BytecodeCompileException(
"Missing member id for ${ref.methodName()}",
@ -2941,6 +2961,7 @@ class BytecodeCompiler(
if (!ref.optionalInvoke()) {
val args = compileCallArgs(ref.arguments(), ref.hasTailBlock()) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, methodId, args.base, encodedCount, dst)
return CompiledValue(dst, SlotType.OBJ)
}
@ -2956,6 +2977,7 @@ class BytecodeCompiler(
)
val args = compileCallArgs(ref.arguments(), ref.hasTailBlock()) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_MEMBER_SLOT, receiver.slot, methodId, args.base, encodedCount, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
@ -3163,7 +3185,11 @@ class BytecodeCompiler(
}
private fun compileBlock(name: String, stmt: BlockStatement): CmdFunction? {
val result = emitBlock(stmt, true) ?: return null
val result = if (shouldInlineBlock(stmt)) {
emitInlineStatements(stmt.statements(), true)
} else {
emitBlock(stmt, true)
} ?: return null
builder.emit(Opcode.RET, result.slot)
val localCount = maxOf(nextSlot, result.slot + 1) - scopeSlotCount
return builder.build(
@ -3217,6 +3243,7 @@ class BytecodeCompiler(
private fun compileStatementValueOrFallback(stmt: Statement, needResult: Boolean = true): CompiledValue? {
val target = if (stmt is BytecodeStatement) stmt.original else stmt
setPos(target.pos)
return if (needResult) {
when (target) {
is ExpressionStatement -> compileRefWithFallback(target.ref, null, target.pos)
@ -3307,6 +3334,9 @@ class BytecodeCompiler(
}
private fun emitBlock(stmt: BlockStatement, needResult: Boolean): CompiledValue? {
if (shouldInlineBlock(stmt)) {
return emitInlineStatements(stmt.statements(), needResult)
}
val captureNames = if (stmt.captureSlots.isEmpty()) emptyList() else stmt.captureSlots.map { it.name }
val planId = builder.addConst(BytecodeConst.SlotPlan(stmt.slotPlan, captureNames))
builder.emit(Opcode.PUSH_SCOPE, planId)
@ -3386,6 +3416,10 @@ class BytecodeCompiler(
private fun emitInlineBlock(stmt: BlockStatement, needResult: Boolean): CompiledValue? =
emitInlineStatements(stmt.statements(), needResult)
private fun shouldInlineBlock(stmt: BlockStatement): Boolean {
return allowLocalSlots && !forceScopeSlots
}
private fun compileInlineBlock(name: String, stmt: net.sergeych.lyng.InlineBlockStatement): CmdFunction? {
val result = emitInlineStatements(stmt.statements(), true) ?: return null
builder.emit(Opcode.RET, result.slot)
@ -3406,7 +3440,7 @@ class BytecodeCompiler(
private fun compileLoopBody(stmt: Statement, needResult: Boolean): CompiledValue? {
val target = if (stmt is BytecodeStatement) stmt.original else stmt
if (target is BlockStatement) {
val useInline = target.slotPlan.isEmpty() && target.captureSlots.isEmpty()
val useInline = !forceScopeSlots && target.slotPlan.isEmpty() && target.captureSlots.isEmpty()
return if (useInline) emitInlineBlock(target, needResult) else emitBlock(target, needResult)
}
return compileStatementValueOrFallback(target, needResult)
@ -3415,17 +3449,18 @@ class BytecodeCompiler(
private fun emitVarDecl(stmt: VarDeclStatement): CompiledValue? {
updateNameObjClass(stmt.name, stmt.initializer, stmt.initializerObjClass)
val scopeId = stmt.scopeId ?: 0
val isModuleSlot = isModuleSlot(scopeId, stmt.name)
val scopeSlot = stmt.slotIndex?.let { slotIndex ->
val key = ScopeSlotKey(scopeId, slotIndex)
scopeSlotMap[key]
} ?: run {
if (scopeId == 0) {
if (isModuleSlot) {
scopeSlotIndexByName[stmt.name]
} else {
null
}
}
if (scopeId == 0 && scopeSlot != null) {
if (isModuleSlot && scopeSlot != null) {
val value = stmt.initializer?.let { compileStatementValueOrFallback(it) } ?: run {
val unsetId = builder.addConst(BytecodeConst.ObjRef(ObjUnset))
builder.emit(Opcode.CONST_OBJ, unsetId, scopeSlot)
@ -3469,15 +3504,18 @@ class BytecodeCompiler(
updateSlotType(localSlot, value.type)
updateSlotObjClass(localSlot, stmt.initializer, stmt.initializerObjClass)
updateNameObjClassFromSlot(stmt.name, localSlot)
val declId = builder.addConst(
BytecodeConst.LocalDecl(
stmt.name,
stmt.isMutable,
stmt.visibility,
stmt.isTransient
val shadowedScopeSlot = scopeSlotIndexByName.containsKey(stmt.name)
if (forceScopeSlots || !shadowedScopeSlot) {
val declId = builder.addConst(
BytecodeConst.LocalDecl(
stmt.name,
stmt.isMutable,
stmt.visibility,
stmt.isTransient
)
)
)
builder.emit(Opcode.DECL_LOCAL, declId, localSlot)
builder.emit(Opcode.DECL_LOCAL, declId, localSlot)
}
return CompiledValue(localSlot, value.type)
}
if (scopeSlot != null) {
@ -3605,15 +3643,11 @@ class BytecodeCompiler(
rangeRef = extractRangeFromLocal(stmt.source)
}
val typedRangeLocal = if (range == null && rangeRef == null) extractTypedRangeLocal(stmt.source) else null
val useLoopScope = stmt.loopSlotPlan.isNotEmpty()
val planId = if (useLoopScope) {
builder.addConst(BytecodeConst.SlotPlan(stmt.loopSlotPlan, emptyList()))
} else {
-1
}
val loopSlotPlan = stmt.loopSlotPlan
var useLoopScope = loopSlotPlan.isNotEmpty()
val loopLocalIndex = localSlotIndexByName[stmt.loopVarName]
var usedOverride = false
val loopSlotId = when {
var loopSlotId = when {
loopLocalIndex != null -> scopeSlotCount + loopLocalIndex
else -> {
val localKey = localSlotInfoMap.entries.firstOrNull { it.value.name == stmt.loopVarName }?.key
@ -3629,7 +3663,36 @@ class BytecodeCompiler(
usedOverride = true
slot
}
val loopDeclId = if (usedOverride) {
var emitDeclLocal = usedOverride
if (useLoopScope && !forceScopeSlots) {
val loopVarOnly = loopSlotPlan.size == 1 && loopSlotPlan.containsKey(stmt.loopVarName)
val loopVarIsLocal = loopSlotId >= scopeSlotCount
if (loopVarOnly && loopVarIsLocal) {
useLoopScope = false
}
}
if (useLoopScope && allowLocalSlots && !forceScopeSlots) {
val needsScope = allowedScopeNames?.let { names ->
loopSlotPlan.keys.any { names.contains(it) }
} == true
if (!needsScope) {
useLoopScope = false
}
}
emitDeclLocal = emitDeclLocal && useLoopScope
if (!forceScopeSlots && loopSlotId < scopeSlotCount) {
val localSlot = allocSlot()
loopSlotOverrides[stmt.loopVarName] = localSlot
usedOverride = true
emitDeclLocal = false
loopSlotId = localSlot
}
val planId = if (useLoopScope) {
builder.addConst(BytecodeConst.SlotPlan(loopSlotPlan, emptyList()))
} else {
-1
}
val loopDeclId = if (emitDeclLocal) {
builder.addConst(
BytecodeConst.LocalDecl(
stmt.loopVarName,
@ -3702,7 +3765,7 @@ class BytecodeCompiler(
builder.emit(Opcode.MOVE_OBJ, nextObj.slot, loopSlotId)
updateSlotType(loopSlotId, SlotType.OBJ)
updateSlotTypeByName(stmt.loopVarName, SlotType.OBJ)
if (usedOverride) {
if (emitDeclLocal) {
builder.emit(Opcode.DECL_LOCAL, loopDeclId, loopSlotId)
}
@ -3809,7 +3872,7 @@ class BytecodeCompiler(
builder.emit(Opcode.MOVE_INT, iSlot, loopSlotId)
updateSlotType(loopSlotId, SlotType.INT)
updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
if (usedOverride) {
if (emitDeclLocal) {
builder.emit(Opcode.DECL_LOCAL, loopDeclId, loopSlotId)
}
loopStack.addLast(
@ -3886,7 +3949,7 @@ class BytecodeCompiler(
builder.emit(Opcode.MOVE_INT, iSlot, loopSlotId)
updateSlotType(loopSlotId, SlotType.INT)
updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
if (usedOverride) {
if (emitDeclLocal) {
builder.emit(Opcode.DECL_LOCAL, loopDeclId, loopSlotId)
}
loopStack.addLast(
@ -4414,7 +4477,27 @@ class BytecodeCompiler(
}
}
private fun setPos(pos: Pos?) {
currentPos = pos
builder.setPos(pos)
}
private fun callSitePos(): Pos = currentPos ?: Pos.builtIn
private fun refPosOrCurrent(ref: ObjRef): Pos {
val refPos = when (ref) {
is LocalVarRef -> ref.pos()
is LocalSlotRef -> ref.pos()
is QualifiedThisRef -> ref.pos()
is ImplicitThisMethodCallRef -> ref.pos()
is StatementRef -> ref.statement.pos
else -> null
}
return refPos ?: callSitePos()
}
private fun compileRefWithFallback(ref: ObjRef, forceType: SlotType?, pos: Pos): CompiledValue? {
setPos(pos)
var compiled = compileRef(ref)
if (compiled != null) {
if (forceType == null) return compiled
@ -4706,8 +4789,9 @@ class BytecodeCompiler(
private fun refPos(ref: BinaryOpRef): Pos = Pos.builtIn
private fun resolveSlot(ref: LocalSlotRef): Int? {
loopSlotOverrides[ref.name]?.let { return it }
val scopeId = refScopeId(ref)
if (scopeId == 0) {
if (isModuleSlot(scopeId, ref.name)) {
val key = ScopeSlotKey(scopeId, refSlot(ref))
scopeSlotMap[key]?.let { return it }
scopeSlotIndexByName[ref.name]?.let { return it }
@ -4718,6 +4802,10 @@ class BytecodeCompiler(
if (ownerLocal != null) {
return scopeSlotCount + ownerLocal
}
val nameLocal = localSlotIndexByName[ref.name]
if (nameLocal != null) {
return scopeSlotCount + nameLocal
}
val scopeKey = ScopeSlotKey(refScopeId(ref), refSlot(ref))
return scopeSlotMap[scopeKey]
}
@ -4725,7 +4813,6 @@ class BytecodeCompiler(
val scopeKey = ScopeSlotKey(refScopeId(ref), refSlot(ref))
return scopeSlotMap[scopeKey]
}
loopSlotOverrides[ref.name]?.let { return it }
val localKey = ScopeSlotKey(refScopeId(ref), refSlot(ref))
val localIndex = localSlotIndexByKey[localKey]
if (localIndex != null) return scopeSlotCount + localIndex
@ -4805,7 +4892,7 @@ class BytecodeCompiler(
val name = scopeSlotNameMap[key]
scopeSlotIndices[index] = key.slot
scopeSlotNames[index] = name
scopeSlotIsModule[index] = key.scopeId == 0
scopeSlotIsModule[index] = key.scopeId == (moduleScopeId ?: 0)
scopeSlotMutableMap[key]?.let { scopeSlotMutables[index] = it }
}
if (allowLocalSlots && localSlotInfoMap.isNotEmpty()) {
@ -4878,7 +4965,8 @@ class BytecodeCompiler(
slotInitClassByKey[ScopeSlotKey(scopeId, slotIndex)] = cls
}
}
if (allowLocalSlots && !forceScopeSlots && slotIndex != null && scopeId != 0) {
val isModuleSlot = isModuleSlot(scopeId, stmt.name)
if (allowLocalSlots && !forceScopeSlots && slotIndex != null && !isModuleSlot) {
val key = ScopeSlotKey(scopeId, slotIndex)
declaredLocalKeys.add(key)
if (!localSlotInfoMap.containsKey(key)) {
@ -5052,6 +5140,13 @@ class BytecodeCompiler(
}
}
private fun isModuleSlot(scopeId: Int, name: String?): Boolean {
val moduleId = moduleScopeId ?: 0
if (scopeId != moduleId) return false
if (allowedScopeNames == null || name == null) return true
return allowedScopeNames.contains(name)
}
private fun collectLoopVarNames(stmt: Statement) {
if (stmt is BytecodeStatement) {
collectLoopVarNames(stmt.original)
@ -5154,7 +5249,7 @@ class BytecodeCompiler(
return
}
val shouldLocalize = !forceScopeSlots || intLoopVarNames.contains(ref.name)
val isModuleSlot = scopeId == 0
val isModuleSlot = isModuleSlot(scopeId, ref.name)
if (allowLocalSlots && !ref.isDelegated && shouldLocalize && !isModuleSlot) {
if (!localSlotInfoMap.containsKey(key)) {
localSlotInfoMap[key] = LocalSlotInfo(ref.name, ref.isMutable)
@ -5203,7 +5298,7 @@ class BytecodeCompiler(
}
} else {
val shouldLocalize = !forceScopeSlots || intLoopVarNames.contains(target.name)
val isModuleSlot = scopeId == 0
val isModuleSlot = isModuleSlot(scopeId, target.name)
if (allowLocalSlots && !target.isDelegated && shouldLocalize && !isModuleSlot) {
if (!localSlotInfoMap.containsKey(key)) {
localSlotInfoMap[key] = LocalSlotInfo(target.name, target.isMutable)

View File

@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -12,17 +12,19 @@
* 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.FrameAccess
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjNull
class BytecodeFrame(
val localCount: Int,
val argCount: Int,
) {
) : FrameAccess {
val slotCount: Int = localCount + argCount
val argBase: Int = localCount
@ -33,31 +35,31 @@ class BytecodeFrame(
private val boolSlots: BooleanArray = BooleanArray(slotCount)
fun getSlotType(slot: Int): SlotType = SlotType.values().first { it.code == slotTypes[slot] }
fun getSlotTypeCode(slot: Int): Byte = slotTypes[slot]
override fun getSlotTypeCode(slot: Int): Byte = slotTypes[slot]
fun setSlotType(slot: Int, type: SlotType) {
slotTypes[slot] = type.code
}
fun getObj(slot: Int): Obj = objSlots[slot] ?: ObjNull
fun setObj(slot: Int, value: Obj) {
override fun getObj(slot: Int): Obj = objSlots[slot] ?: ObjNull
override fun setObj(slot: Int, value: Obj) {
objSlots[slot] = value
slotTypes[slot] = SlotType.OBJ.code
}
fun getInt(slot: Int): Long = intSlots[slot]
fun setInt(slot: Int, value: Long) {
override fun getInt(slot: Int): Long = intSlots[slot]
override fun setInt(slot: Int, value: Long) {
intSlots[slot] = value
slotTypes[slot] = SlotType.INT.code
}
fun getReal(slot: Int): Double = realSlots[slot]
fun setReal(slot: Int, value: Double) {
override fun getReal(slot: Int): Double = realSlots[slot]
override fun setReal(slot: Int, value: Double) {
realSlots[slot] = value
slotTypes[slot] = SlotType.REAL.code
}
fun getBool(slot: Int): Boolean = boolSlots[slot]
fun setBool(slot: Int, value: Boolean) {
override fun getBool(slot: Int): Boolean = boolSlots[slot]
override fun setBool(slot: Int, value: Boolean) {
boolSlots[slot] = value
slotTypes[slot] = SlotType.BOOL.code
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -12,23 +12,14 @@
* 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.Pos
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
import net.sergeych.lyng.DestructuringVarDeclStatement
import net.sergeych.lyng.WhenCase
import net.sergeych.lyng.WhenCondition
import net.sergeych.lyng.WhenEqualsCondition
import net.sergeych.lyng.WhenInCondition
import net.sergeych.lyng.WhenIsCondition
import net.sergeych.lyng.WhenStatement
import net.sergeych.lyng.*
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.RangeRef
class BytecodeStatement private constructor(
val original: Statement,
@ -51,13 +42,14 @@ class BytecodeStatement private constructor(
returnLabels: Set<String> = emptySet(),
rangeLocalNames: Set<String> = emptySet(),
allowedScopeNames: Set<String>? = null,
moduleScopeId: Int? = 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"
val statementName = statement.toString()
throw BytecodeCompileException(
"Bytecode compile error: unsupported statement $statementName in '$nameHint'",
statement.pos
@ -69,6 +61,7 @@ class BytecodeStatement private constructor(
returnLabels = returnLabels,
rangeLocalNames = rangeLocalNames,
allowedScopeNames = allowedScopeNames,
moduleScopeId = moduleScopeId,
slotTypeByScopeId = slotTypeByScopeId,
knownNameObjClass = knownNameObjClass
)

View File

@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -12,6 +12,7 @@
* 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
@ -27,9 +28,11 @@ class CmdBuilder {
data class Instr(val op: Opcode, val operands: List<Operand>)
private val instructions = mutableListOf<Instr>()
private val posByInstr = mutableListOf<net.sergeych.lyng.Pos?>()
private val constPool = mutableListOf<BytecodeConst>()
private val labelPositions = mutableMapOf<Label, Int>()
private var nextLabelId = 0
private var currentPos: net.sergeych.lyng.Pos? = null
fun addConst(c: BytecodeConst): Int {
constPool += c
@ -38,10 +41,16 @@ class CmdBuilder {
fun emit(op: Opcode, vararg operands: Int) {
instructions += Instr(op, operands.map { Operand.IntVal(it) })
posByInstr += currentPos
}
fun emit(op: Opcode, operands: List<Operand>) {
instructions += Instr(op, operands)
posByInstr += currentPos
}
fun setPos(pos: net.sergeych.lyng.Pos?) {
currentPos = pos
}
fun label(): Label = Label(nextLabelId++)
@ -103,7 +112,8 @@ class CmdBuilder {
localSlotNames = localSlotNames,
localSlotMutables = localSlotMutables,
constants = constPool.toList(),
cmds = cmds.toTypedArray()
cmds = cmds.toTypedArray(),
posByIp = posByInstr.toTypedArray()
)
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -12,6 +12,7 @@
* 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
@ -29,6 +30,7 @@ data class CmdFunction(
val localSlotMutables: BooleanArray,
val constants: List<BytecodeConst>,
val cmds: Array<Cmd>,
val posByIp: Array<net.sergeych.lyng.Pos?>,
) {
init {
require(scopeSlotIndices.size == scopeSlotCount) { "scopeSlotIndices size mismatch" }
@ -37,5 +39,8 @@ data class CmdFunction(
require(localSlotNames.size == localSlotMutables.size) { "localSlot metadata size mismatch" }
require(localSlotNames.size <= localCount) { "localSlotNames exceed localCount" }
require(addrCount >= 0) { "addrCount must be non-negative" }
if (posByIp.isNotEmpty()) {
require(posByIp.size == cmds.size) { "posByIp size mismatch" }
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2026 Sergey S. Chernov
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -12,17 +12,12 @@
* 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.ModuleScope
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.Pos
import net.sergeych.lyng.ReturnException
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
import net.sergeych.lyng.*
import net.sergeych.lyng.obj.*
class CmdVm {
@ -152,7 +147,7 @@ class CmdConstBool(internal val constId: Int, internal val dst: Int) : Cmd() {
class CmdLoadThis(internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
frame.setObj(dst, frame.scope.thisObj)
frame.setObj(dst, frame.ensureScope().thisObj)
return
}
}
@ -165,8 +160,8 @@ class CmdLoadThisVariant(
val typeConst = frame.fn.constants.getOrNull(typeId) as? BytecodeConst.StringVal
?: error("LOAD_THIS_VARIANT expects StringVal at $typeId")
val typeName = typeConst.value
val receiver = frame.scope.thisVariants.firstOrNull { it.isInstanceOf(typeName) }
?: frame.scope.raiseClassCastError("Cannot cast ${frame.scope.thisObj.objClass.className} to $typeName")
val receiver = frame.ensureScope().thisVariants.firstOrNull { it.isInstanceOf(typeName) }
?: frame.ensureScope().raiseClassCastError("Cannot cast ${frame.ensureScope().thisObj.objClass.className} to $typeName")
frame.setObj(dst, receiver)
return
}
@ -222,11 +217,11 @@ class CmdAssertIs(internal val objSlot: Int, internal val typeSlot: Int) : Cmd()
override suspend fun perform(frame: CmdFrame) {
val obj = frame.slotToObj(objSlot)
val typeObj = frame.slotToObj(typeSlot)
val clazz = typeObj as? ObjClass ?: frame.scope.raiseClassCastError(
"${typeObj.inspect(frame.scope)} is not the class instance"
val clazz = typeObj as? ObjClass ?: frame.ensureScope().raiseClassCastError(
"${typeObj.inspect(frame.ensureScope())} is not the class instance"
)
if (!obj.isInstanceOf(clazz)) {
frame.scope.raiseClassCastError("expected ${clazz.className}, got ${obj.objClass.className}")
frame.ensureScope().raiseClassCastError("expected ${clazz.className}, got ${obj.objClass.className}")
}
return
}
@ -240,8 +235,8 @@ class CmdMakeQualifiedView(
override suspend fun perform(frame: CmdFrame) {
val obj0 = frame.slotToObj(objSlot)
val typeObj = frame.slotToObj(typeSlot)
val clazz = typeObj as? ObjClass ?: frame.scope.raiseClassCastError(
"${typeObj.inspect(frame.scope)} is not the class instance"
val clazz = typeObj as? ObjClass ?: frame.ensureScope().raiseClassCastError(
"${typeObj.inspect(frame.ensureScope())} is not the class instance"
)
val base = when (obj0) {
is ObjQualifiedView -> obj0.instance
@ -787,7 +782,7 @@ class CmdCmpEqObj(internal val a: Int, internal val b: Int, internal val dst: In
override suspend fun perform(frame: CmdFrame) {
val left = frame.slotToObj(a)
val right = frame.slotToObj(b)
frame.setBool(dst, left.equals(frame.scope, right))
frame.setBool(dst, left.equals(frame.ensureScope(), right))
return
}
}
@ -796,7 +791,7 @@ class CmdCmpNeqObj(internal val a: Int, internal val b: Int, internal val dst: I
override suspend fun perform(frame: CmdFrame) {
val left = frame.slotToObj(a)
val right = frame.slotToObj(b)
frame.setBool(dst, !left.equals(frame.scope, right))
frame.setBool(dst, !left.equals(frame.ensureScope(), right))
return
}
}
@ -838,28 +833,28 @@ class CmdOrBool(internal val a: Int, internal val b: Int, internal val dst: Int)
class CmdCmpLtObj(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
frame.setBool(dst, frame.slotToObj(a).compareTo(frame.scope, frame.slotToObj(b)) < 0)
frame.setBool(dst, frame.slotToObj(a).compareTo(frame.ensureScope(), frame.slotToObj(b)) < 0)
return
}
}
class CmdCmpLteObj(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
frame.setBool(dst, frame.slotToObj(a).compareTo(frame.scope, frame.slotToObj(b)) <= 0)
frame.setBool(dst, frame.slotToObj(a).compareTo(frame.ensureScope(), frame.slotToObj(b)) <= 0)
return
}
}
class CmdCmpGtObj(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
frame.setBool(dst, frame.slotToObj(a).compareTo(frame.scope, frame.slotToObj(b)) > 0)
frame.setBool(dst, frame.slotToObj(a).compareTo(frame.ensureScope(), frame.slotToObj(b)) > 0)
return
}
}
class CmdCmpGteObj(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
frame.setBool(dst, frame.slotToObj(a).compareTo(frame.scope, frame.slotToObj(b)) >= 0)
frame.setBool(dst, frame.slotToObj(a).compareTo(frame.ensureScope(), frame.slotToObj(b)) >= 0)
return
}
}
@ -883,7 +878,7 @@ class CmdAddObj(internal val a: Int, internal val b: Int, internal val dst: Int)
return
}
}
frame.setObj(dst, frame.slotToObj(a).plus(frame.scope, frame.slotToObj(b)))
frame.setObj(dst, frame.slotToObj(a).plus(frame.ensureScope(), frame.slotToObj(b)))
return
}
}
@ -907,7 +902,7 @@ class CmdSubObj(internal val a: Int, internal val b: Int, internal val dst: Int)
return
}
}
frame.setObj(dst, frame.slotToObj(a).minus(frame.scope, frame.slotToObj(b)))
frame.setObj(dst, frame.slotToObj(a).minus(frame.ensureScope(), frame.slotToObj(b)))
return
}
}
@ -931,7 +926,7 @@ class CmdMulObj(internal val a: Int, internal val b: Int, internal val dst: Int)
return
}
}
frame.setObj(dst, frame.slotToObj(a).mul(frame.scope, frame.slotToObj(b)))
frame.setObj(dst, frame.slotToObj(a).mul(frame.ensureScope(), frame.slotToObj(b)))
return
}
}
@ -955,7 +950,7 @@ class CmdDivObj(internal val a: Int, internal val b: Int, internal val dst: Int)
return
}
}
frame.setObj(dst, frame.slotToObj(a).div(frame.scope, frame.slotToObj(b)))
frame.setObj(dst, frame.slotToObj(a).div(frame.ensureScope(), frame.slotToObj(b)))
return
}
}
@ -979,14 +974,14 @@ class CmdModObj(internal val a: Int, internal val b: Int, internal val dst: Int)
return
}
}
frame.setObj(dst, frame.slotToObj(a).mod(frame.scope, frame.slotToObj(b)))
frame.setObj(dst, frame.slotToObj(a).mod(frame.ensureScope(), frame.slotToObj(b)))
return
}
}
class CmdContainsObj(internal val target: Int, internal val value: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
frame.setBool(dst, frame.slotToObj(target).contains(frame.scope, frame.slotToObj(value)))
frame.setBool(dst, frame.slotToObj(target).contains(frame.ensureScope(), frame.slotToObj(value)))
return
}
}
@ -1002,17 +997,17 @@ class CmdAssignOpObj(
val target = frame.slotToObj(targetSlot)
val value = frame.slotToObj(valueSlot)
val result = when (BinOp.values().getOrNull(opId)) {
BinOp.PLUS -> target.plusAssign(frame.scope, value)
BinOp.MINUS -> target.minusAssign(frame.scope, value)
BinOp.STAR -> target.mulAssign(frame.scope, value)
BinOp.SLASH -> target.divAssign(frame.scope, value)
BinOp.PERCENT -> target.modAssign(frame.scope, value)
BinOp.PLUS -> target.plusAssign(frame.ensureScope(), value)
BinOp.MINUS -> target.minusAssign(frame.ensureScope(), value)
BinOp.STAR -> target.mulAssign(frame.ensureScope(), value)
BinOp.SLASH -> target.divAssign(frame.ensureScope(), value)
BinOp.PERCENT -> target.modAssign(frame.ensureScope(), value)
else -> null
}
if (result == null) {
val name = (frame.fn.constants.getOrNull(nameId) as? BytecodeConst.StringVal)?.value
if (name != null) frame.scope.raiseIllegalAssignment("symbol is readonly: $name")
frame.scope.raiseIllegalAssignment("symbol is readonly")
if (name != null) frame.ensureScope().raiseIllegalAssignment("symbol is readonly: $name")
frame.ensureScope().raiseIllegalAssignment("symbol is readonly")
}
frame.storeObjResult(dst, result)
return
@ -1118,7 +1113,7 @@ class CmdDeclLocal(internal val constId: Int, internal val slot: Int) : Cmd() {
val decl = frame.fn.constants[constId] as? BytecodeConst.LocalDecl
?: error("DECL_LOCAL expects LocalDecl at $constId")
val value = frame.slotToObj(slot).byValueCopy()
frame.scope.addItem(
frame.ensureScope().addItem(
decl.name,
decl.isMutable,
value,
@ -1134,12 +1129,12 @@ class CmdDeclExtProperty(internal val constId: Int, internal val slot: Int) : Cm
override suspend fun perform(frame: CmdFrame) {
val decl = frame.fn.constants[constId] as? BytecodeConst.ExtensionPropertyDecl
?: error("DECL_EXT_PROPERTY expects ExtensionPropertyDecl at $constId")
val type = frame.scope[decl.extTypeName]?.value
?: frame.scope.raiseSymbolNotFound("class ${decl.extTypeName} not found")
val type = frame.ensureScope()[decl.extTypeName]?.value
?: frame.ensureScope().raiseSymbolNotFound("class ${decl.extTypeName} not found")
if (type !is ObjClass) {
frame.scope.raiseClassCastError("${decl.extTypeName} is not the class instance")
frame.ensureScope().raiseClassCastError("${decl.extTypeName} is not the class instance")
}
frame.scope.addExtension(
frame.ensureScope().addExtension(
type,
decl.property.name,
ObjRecord(
@ -1164,16 +1159,16 @@ class CmdCallDirect(
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncFrameToScope()
frame.syncFrameToScope(useRefs = true)
}
val ref = frame.fn.constants.getOrNull(id) as? BytecodeConst.ObjRef
?: error("CALL_DIRECT expects ObjRef at $id")
val callee = ref.value
val args = frame.buildArguments(argBase, argCount)
val result = if (PerfFlags.SCOPE_POOL) {
frame.scope.withChildFrame(args) { child -> callee.callOn(child) }
frame.ensureScope().withChildFrame(args) { child -> callee.callOn(child) }
} else {
callee.callOn(frame.scope.createChildScope(frame.scope.pos, args = args))
callee.callOn(frame.ensureScope().createChildScope(frame.ensureScope().pos, args = args))
}
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncScopeToFrame()
@ -1191,7 +1186,7 @@ class CmdCallSlot(
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncFrameToScope()
frame.syncFrameToScope(useRefs = true)
}
val callee = frame.slotToObj(calleeSlot)
if (callee === ObjUnset) {
@ -1203,15 +1198,15 @@ class CmdCallSlot(
}
val message = name?.let { "property '$it' is unset (not initialized)" }
?: "property is unset (not initialized) in ${frame.fn.name} at slot $calleeSlot"
frame.scope.raiseUnset(message)
frame.ensureScope().raiseUnset(message)
}
val args = frame.buildArguments(argBase, argCount)
val canPool = PerfFlags.SCOPE_POOL && callee !is Statement
val result = if (canPool) {
frame.scope.withChildFrame(args) { child -> callee.callOn(child) }
frame.ensureScope().withChildFrame(args) { child -> callee.callOn(child) }
} else {
// Pooling for Statement-based callables (lambdas) can still alter closure semantics; keep safe path for now.
callee.callOn(frame.scope.createChildScope(frame.scope.pos, args = args))
callee.callOn(frame.ensureScope().createChildScope(frame.ensureScope().pos, args = args))
}
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncScopeToFrame()
@ -1239,7 +1234,7 @@ class CmdListLiteral(
list.ensureCapacity(list.size + value.list.size)
list.addAll(value.list)
}
else -> frame.scope.raiseError("Spread element must be list")
else -> frame.ensureScope().raiseError("Spread element must be list")
}
} else {
list.add(value)
@ -1266,14 +1261,14 @@ class CmdGetMemberSlot(
if (methodId >= 0) {
inst?.methodRecordForId(methodId) ?: receiver.objClass.methodRecordForId(methodId)
} else null
} ?: frame.scope.raiseSymbolNotFound("member")
} ?: frame.ensureScope().raiseSymbolNotFound("member")
val name = rec.memberName ?: "<member>"
if (receiver is ObjQualifiedView) {
val resolved = receiver.readField(frame.scope, name)
val resolved = receiver.readField(frame.ensureScope(), name)
frame.storeObjResult(dst, resolved.value)
return
}
val resolved = receiver.resolveRecord(frame.scope, rec, name, rec.declaringClass)
val resolved = receiver.resolveRecord(frame.ensureScope(), rec, name, rec.declaringClass)
frame.storeObjResult(dst, resolved.value)
return
}
@ -1295,13 +1290,13 @@ class CmdSetMemberSlot(
if (methodId >= 0) {
inst?.methodRecordForId(methodId) ?: receiver.objClass.methodRecordForId(methodId)
} else null
} ?: frame.scope.raiseSymbolNotFound("member")
} ?: frame.ensureScope().raiseSymbolNotFound("member")
val name = rec.memberName ?: "<member>"
if (receiver is ObjQualifiedView) {
receiver.writeField(frame.scope, name, frame.slotToObj(valueSlot))
receiver.writeField(frame.ensureScope(), name, frame.slotToObj(valueSlot))
return
}
frame.scope.assign(rec, name, frame.slotToObj(valueSlot))
frame.ensureScope().assign(rec, name, frame.slotToObj(valueSlot))
return
}
}
@ -1315,17 +1310,17 @@ class CmdCallMemberSlot(
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncFrameToScope()
frame.syncFrameToScope(useRefs = true)
}
val receiver = frame.slotToObj(recvSlot)
val inst = receiver as? ObjInstance
val rec = inst?.methodRecordForId(methodId)
?: receiver.objClass.methodRecordForId(methodId)
?: frame.scope.raiseError("member id $methodId not found on ${receiver.objClass.className}")
?: frame.ensureScope().raiseError("member id $methodId not found on ${receiver.objClass.className}")
val callArgs = frame.buildArguments(argBase, argCount)
val name = rec.memberName ?: "<member>"
if (receiver is ObjQualifiedView) {
val result = receiver.invokeInstanceMethod(frame.scope, name, callArgs)
val result = receiver.invokeInstanceMethod(frame.ensureScope(), name, callArgs)
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncScopeToFrame()
}
@ -1335,14 +1330,14 @@ class CmdCallMemberSlot(
val decl = rec.declaringClass ?: receiver.objClass
val result = when (rec.type) {
ObjRecord.Type.Property -> {
if (callArgs.isEmpty()) (rec.value as ObjProperty).callGetter(frame.scope, receiver, decl)
else frame.scope.raiseError("property $name cannot be called with arguments")
if (callArgs.isEmpty()) (rec.value as ObjProperty).callGetter(frame.ensureScope(), receiver, decl)
else frame.ensureScope().raiseError("property $name cannot be called with arguments")
}
ObjRecord.Type.Fun, ObjRecord.Type.Delegated -> {
val callScope = inst?.instanceScope ?: frame.scope
val callScope = inst?.instanceScope ?: frame.ensureScope()
rec.value.invoke(callScope, receiver, callArgs, decl)
}
else -> frame.scope.raiseError("member $name is not callable")
else -> frame.ensureScope().raiseError("member $name is not callable")
}
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncScopeToFrame()
@ -1358,7 +1353,7 @@ class CmdGetIndex(
internal val dst: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val result = frame.slotToObj(targetSlot).getAt(frame.scope, frame.slotToObj(indexSlot))
val result = frame.slotToObj(targetSlot).getAt(frame.ensureScope(), frame.slotToObj(indexSlot))
frame.storeObjResult(dst, result)
return
}
@ -1370,7 +1365,7 @@ class CmdSetIndex(
internal val valueSlot: Int,
) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
frame.slotToObj(targetSlot).putAt(frame.scope, frame.slotToObj(indexSlot), frame.slotToObj(valueSlot))
frame.slotToObj(targetSlot).putAt(frame.ensureScope(), frame.slotToObj(indexSlot), frame.slotToObj(valueSlot))
return
}
}
@ -1378,11 +1373,11 @@ class CmdSetIndex(
class CmdEvalRef(internal val id: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncFrameToScope()
frame.syncFrameToScope(useRefs = true)
}
val ref = frame.fn.constants[id] as? BytecodeConst.Ref
?: error("EVAL_REF expects Ref at $id")
val result = ref.value.evalValue(frame.scope)
val result = ref.value.evalValue(frame.ensureScope())
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncScopeToFrame()
}
@ -1394,11 +1389,11 @@ class CmdEvalRef(internal val id: Int, internal val dst: Int) : Cmd() {
class CmdEvalStmt(internal val id: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncFrameToScope()
frame.syncFrameToScope(useRefs = true)
}
val stmt = frame.fn.constants.getOrNull(id) as? BytecodeConst.StatementVal
?: error("EVAL_STMT expects StatementVal at $id")
val result = stmt.statement.execute(frame.scope)
val result = stmt.statement.execute(frame.ensureScope())
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncScopeToFrame()
}
@ -1410,11 +1405,11 @@ class CmdEvalStmt(internal val id: Int, internal val dst: Int) : Cmd() {
class CmdMakeValueFn(internal val id: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncFrameToScope()
frame.syncFrameToScope(useRefs = true)
}
val valueFn = frame.fn.constants.getOrNull(id) as? BytecodeConst.ValueFn
?: error("MAKE_VALUE_FN expects ValueFn at $id")
val result = valueFn.fn(frame.scope).value
val result = valueFn.fn(frame.ensureScope()).value
if (frame.fn.localSlotNames.isNotEmpty()) {
frame.syncScopeToFrame()
}
@ -1458,6 +1453,8 @@ class CmdFrame(
var ip: Int = 0
var scope: Scope = scope0
private val moduleScope: Scope = resolveModuleScope(scope0)
private val scopeSlotNames: Set<String> = fn.scopeSlotNames.filterNotNull().toSet()
private var lastScopePosIp = -1
internal val scopeStack = ArrayDeque<Scope>()
internal val scopeVirtualStack = ArrayDeque<Boolean>()
@ -1504,10 +1501,24 @@ class CmdFrame(
return last
}
fun ensureScope(): Scope {
val pos = posForIp(ip - 1)
if (pos != null && lastScopePosIp != ip) {
scope.pos = pos
lastScopePosIp = ip
}
return scope
}
private fun posForIp(ip: Int): Pos? {
if (ip < 0) return null
return fn.posByIp.getOrNull(ip)
}
fun pushScope(plan: Map<String, Int>, captures: List<String>) {
val parentScope = scope
if (captures.isNotEmpty()) {
syncFrameToScope()
syncFrameToScope(useRefs = true)
}
val captureRecords = if (captures.isNotEmpty()) {
captures.map { name ->
@ -1555,7 +1566,7 @@ class CmdFrame(
val captures = captureStack.removeLastOrNull() ?: emptyList()
scopeDepth -= 1
if (captures.isNotEmpty()) {
syncFrameToScope()
syncFrameToScope(useRefs = true)
}
}
@ -1569,13 +1580,13 @@ class CmdFrame(
suspend fun cancelTopIterator() {
val iter = iterStack.removeLastOrNull() ?: return
iter.invokeInstanceMethod(scope, "cancelIteration") { ObjVoid }
iter.invokeInstanceMethod(ensureScope(), "cancelIteration") { ObjVoid }
}
suspend fun cancelIterators() {
while (iterStack.isNotEmpty()) {
val iter = iterStack.removeLast()
iter.invokeInstanceMethod(scope, "cancelIteration") { ObjVoid }
iter.invokeInstanceMethod(ensureScope(), "cancelIteration") { ObjVoid }
}
}
@ -1781,7 +1792,7 @@ class CmdFrame(
suspend fun throwObj(pos: Pos, value: Obj) {
var errorObject = value
val throwScope = scope.createChildScope(pos = pos)
val throwScope = ensureScope().createChildScope(pos = pos)
if (errorObject is ObjString) {
errorObject = ObjException(throwScope, errorObject.value).apply { getStackTrace() }
}
@ -1803,18 +1814,21 @@ class CmdFrame(
}
}
fun syncFrameToScope() {
fun syncFrameToScope(useRefs: Boolean = false) {
val names = fn.localSlotNames
if (names.isEmpty()) return
for (i in names.indices) {
val name = names[i] ?: continue
if (scopeSlotNames.contains(name)) continue
val target = resolveLocalScope(i) ?: continue
val value = localSlotToObj(i)
val value = if (useRefs) FrameSlotRef(frame, i) else localSlotToObj(i)
val rec = target.getLocalRecordDirect(name)
if (rec == null) {
val isMutable = fn.localSlotMutables.getOrElse(i) { true }
target.addItem(name, isMutable, value)
} else {
val existing = rec.value
if (existing is FrameSlotRef && !useRefs) continue
rec.value = value
}
}
@ -1828,6 +1842,16 @@ class CmdFrame(
val target = resolveLocalScope(i) ?: continue
val rec = target.getLocalRecordDirect(name) ?: continue
val value = rec.value
if (value is FrameSlotRef) {
val resolved = value.read()
when (resolved) {
is ObjInt -> frame.setInt(i, resolved.value)
is ObjReal -> frame.setReal(i, resolved.value)
is ObjBool -> frame.setBool(i, resolved.value)
else -> frame.setObj(i, resolved)
}
continue
}
when (value) {
is ObjInt -> frame.setInt(i, value.value)
is ObjReal -> frame.setReal(i, value.value)
@ -1856,6 +1880,7 @@ class CmdFrame(
argBase: Int,
plan: BytecodeConst.CallArgsPlan,
): Arguments {
val scope = ensureScope()
val positional = ArrayList<Obj>(plan.specs.size)
var named: LinkedHashMap<String, Obj>? = null
var namedSeen = false
@ -1931,7 +1956,9 @@ class CmdFrame(
val target = scopeTarget(slot)
val index = ensureScopeSlot(target, slot)
val record = target.getSlotRecord(index)
if (record.value !== ObjUnset) return record.value
val direct = record.value
if (direct is FrameSlotRef) return direct.read()
if (direct !== ObjUnset) return direct
val name = fn.scopeSlotNames[slot] ?: return record.value
val resolved = target.get(name) ?: return record.value
if (resolved.value !== ObjUnset) {
@ -1944,7 +1971,9 @@ class CmdFrame(
val target = addrScopes[addrSlot] ?: error("Address slot $addrSlot is not resolved")
val index = addrIndices[addrSlot]
val record = target.getSlotRecord(index)
if (record.value !== ObjUnset) return record.value
val direct = record.value
if (direct is FrameSlotRef) return direct.read()
if (direct !== ObjUnset) return direct
val slotId = addrScopeSlots[addrSlot]
val name = fn.scopeSlotNames[slotId] ?: return record.value
val resolved = target.get(name) ?: return record.value

View File

@ -463,6 +463,7 @@ class CastRef(
/** Qualified `this@Type`: resolves to a view of current `this` starting dispatch from the ancestor Type. */
class QualifiedThisRef(val typeName: String, private val atPos: Pos) : ObjRef {
internal fun pos(): Pos = atPos
override suspend fun get(scope: Scope): ObjRecord {
val t = scope[typeName]?.value as? ObjClass
?: scope.raiseError("unknown type $typeName")
@ -2107,6 +2108,7 @@ class ImplicitThisMethodCallRef(
private val atPos: Pos,
private val preferredThisTypeName: String? = null
) : ObjRef {
internal fun pos(): Pos = atPos
internal fun methodName(): String = name
internal fun arguments(): List<ParsedArgument> = args
internal fun hasTailBlock(): Boolean = tailBlock
@ -2184,13 +2186,38 @@ class LocalSlotRef(
return null
}
private fun resolveOwnerAndSlot(scope: Scope): Pair<Scope, Int>? {
var s: Scope? = scope
var guard = 0
while (s != null && guard++ < 1024) {
val idx = s.getSlotIndexOf(name)
if (idx != null) {
if (idx == slot) return s to slot
if (!strict || captureOwnerSlot != null) return s to idx
}
s = s.parent
}
return null
}
override suspend fun get(scope: Scope): ObjRecord {
scope.pos = atPos
val owner = resolveOwner(scope) ?: scope.raiseError("slot owner not found for $name")
if (slot < 0 || slot >= owner.slotCount()) {
val resolved = resolveOwnerAndSlot(scope)
if (resolved == null) {
val rec = scope.get(name) ?: scope.raiseError("slot owner not found for $name")
if (rec.declaringClass != null &&
!canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)
) {
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
}
return rec
}
val owner = resolved.first
val slotIndex = resolved.second
if (slotIndex < 0 || slotIndex >= owner.slotCount()) {
scope.raiseError("slot index out of range for $name")
}
val rec = owner.getSlotRecord(slot)
val rec = owner.getSlotRecord(slotIndex)
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
}
@ -2199,11 +2226,22 @@ class LocalSlotRef(
override suspend fun evalValue(scope: Scope): Obj {
scope.pos = atPos
val owner = resolveOwner(scope) ?: scope.raiseError("slot owner not found for $name")
if (slot < 0 || slot >= owner.slotCount()) {
val resolved = resolveOwnerAndSlot(scope)
if (resolved == null) {
val rec = scope.get(name) ?: scope.raiseError("slot owner not found for $name")
if (rec.declaringClass != null &&
!canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)
) {
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
}
return scope.resolve(rec, name)
}
val owner = resolved.first
val slotIndex = resolved.second
if (slotIndex < 0 || slotIndex >= owner.slotCount()) {
scope.raiseError("slot index out of range for $name")
}
val rec = owner.getSlotRecord(slot)
val rec = owner.getSlotRecord(slotIndex)
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
}
@ -2212,11 +2250,23 @@ class LocalSlotRef(
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
scope.pos = atPos
val owner = resolveOwner(scope) ?: scope.raiseError("slot owner not found for $name")
if (slot < 0 || slot >= owner.slotCount()) {
val resolved = resolveOwnerAndSlot(scope)
if (resolved == null) {
val rec = scope.get(name) ?: scope.raiseError("slot owner not found for $name")
if (rec.declaringClass != null &&
!canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)
) {
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
}
scope.assign(rec, name, newValue)
return
}
val owner = resolved.first
val slotIndex = resolved.second
if (slotIndex < 0 || slotIndex >= owner.slotCount()) {
scope.raiseError("slot index out of range for $name")
}
val rec = owner.getSlotRecord(slot)
val rec = owner.getSlotRecord(slotIndex)
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
scope.raiseError(ObjIllegalAccessException(scope, "private field access"))
}

View File

@ -7,12 +7,16 @@ Current focus
- Enforce compile-time name/member resolution only; no runtime scope lookup or fallback.
- Bytecode uses memberId-based ops (CALL_MEMBER_SLOT/GET_MEMBER_SLOT/SET_MEMBER_SLOT).
- Runtime lookup opcodes (CALL_VIRTUAL/GET_FIELD/SET_FIELD) and fallback callsites are removed.
- Migrate bytecode to frame-first locals with lazy scope reflection; avoid eager scope slot plans in compiled functions.
- Use FrameSlotRef for captures and only materialize Scope for Kotlin interop; use frame.ip -> pos mapping for diagnostics.
Key recent changes
- Removed method callsite PICs and fallback opcodes; bytecode now relies on compile-time member ids only.
- Operator dispatch emits memberId calls when known; falls back to Obj opcodes for allowed built-ins without name lookup.
- Object members are allowed on unknown types; other members still require a statically known receiver type.
- Renamed BytecodeFallbackException to BytecodeCompileException.
- Added frame.ip -> pos mapping; call-site ops restore pos after args to keep stack traces accurate.
- Loop var overrides now take precedence in slot resolution to keep loop locals in frame slots.
- LocalSlotRef now falls back to name lookup when slot plans are missing (closure safety).
Known failing tests
- None (jvmTest passing).
@ -20,14 +24,12 @@ Known failing tests
Files touched recently
- notes/type_system_spec.md (spec updated)
- AGENTS.md (type inference reminders)
- lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt
- lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt
- lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt
- lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt
- various bytecode runtime/disassembler files (memberId ops)
- lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt
Last test run
- ./gradlew :lynglib:jvmTest --tests ScriptTest.testForInIterableUnknownTypeDisasm
- ./gradlew :lynglib:jvmTest
Spec decisions (notes/type_system_spec.md)
- Nullability: Kotlin-style, T non-null, T? nullable, !! asserts non-null.