From df48a06311ebea83354d607bacad4ce9d3b49898 Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 30 Jan 2026 11:11:43 +0300 Subject: [PATCH] Fix while bytecode scoping and arithmetic fallback --- .../lyng/bytecode/BytecodeCompiler.kt | 137 +++++++++++++++++- .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 90 +++++++----- lynglib/src/commonTest/kotlin/ScriptTest.kt | 9 +- 3 files changed, 191 insertions(+), 45 deletions(-) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 1e18ccf..351a266 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -173,6 +173,13 @@ class BytecodeCompiler( if (!allowLocalSlots) return null if (ref.isDelegated) return null if (ref.name.isEmpty()) return null + if (ref.captureOwnerScopeId == null) { + val byName = scopeSlotIndexByName[ref.name] + if (byName != null) { + val resolved = slotTypes[byName] ?: SlotType.UNKNOWN + return CompiledValue(byName, resolved) + } + } val mapped = resolveSlot(ref) ?: return compileNameLookup(ref.name) var resolved = slotTypes[mapped] ?: SlotType.UNKNOWN if (resolved == SlotType.UNKNOWN && intLoopVarNames.contains(ref.name)) { @@ -191,6 +198,10 @@ class BytecodeCompiler( is LocalVarRef -> { if (allowLocalSlots) { if (!forceScopeSlots) { + scopeSlotIndexByName[ref.name]?.let { slot -> + val resolved = slotTypes[slot] ?: SlotType.UNKNOWN + return CompiledValue(slot, resolved) + } loopSlotOverrides[ref.name]?.let { slot -> val resolved = slotTypes[slot] ?: SlotType.UNKNOWN return CompiledValue(slot, resolved) @@ -415,7 +426,42 @@ class BytecodeCompiler( b = CompiledValue(b.slot, SlotType.INT) } val typesMismatch = a.type != b.type && a.type != SlotType.UNKNOWN && b.type != SlotType.UNKNOWN - if (typesMismatch && op !in setOf(BinOp.EQ, BinOp.NEQ, BinOp.LT, BinOp.LTE, BinOp.GT, BinOp.GTE)) { + 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) + } + 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) + } + if (typesMismatch && !allowMixedNumeric && + op !in setOf(BinOp.EQ, BinOp.NEQ, BinOp.LT, BinOp.LTE, BinOp.GT, BinOp.GTE) + ) { return null } val out = allocSlot() @@ -876,13 +922,32 @@ class BytecodeCompiler( if (!allowLocalSlots) return null if (!localTarget.isMutable || localTarget.isDelegated) return null val value = compileRef(assignValue(ref)) ?: return null - val slot = resolveSlot(localTarget) ?: return null + val resolvedSlot = resolveSlot(localTarget) ?: return null + val slot = if (resolvedSlot < scopeSlotCount && localTarget.captureOwnerScopeId == null) { + localSlotIndexByName[localTarget.name]?.let { scopeSlotCount + it } ?: resolvedSlot + } else { + resolvedSlot + } if (slot < scopeSlotCount && value.type != SlotType.UNKNOWN) { val addrSlot = ensureScopeAddr(slot) emitStoreToAddr(value.slot, addrSlot, value.type) + if (localTarget.captureOwnerScopeId == null) { + localSlotIndexByName[localTarget.name]?.let { mirror -> + val mirrorSlot = scopeSlotCount + mirror + emitMove(value, mirrorSlot) + updateSlotType(mirrorSlot, value.type) + } + } } else if (slot < scopeSlotCount) { val addrSlot = ensureScopeAddr(slot) emitStoreToAddr(value.slot, addrSlot, SlotType.OBJ) + if (localTarget.captureOwnerScopeId == null) { + localSlotIndexByName[localTarget.name]?.let { mirror -> + val mirrorSlot = scopeSlotCount + mirror + emitMove(value, mirrorSlot) + updateSlotType(mirrorSlot, value.type) + } + } } else { when (value.type) { SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, slot) @@ -2119,13 +2184,48 @@ class BytecodeCompiler( private fun compileLoopBody(stmt: Statement, needResult: Boolean): CompiledValue? { val target = if (stmt is BytecodeStatement) stmt.original else stmt - return if (target is BlockStatement) emitInlineBlock(target, needResult) - else compileStatementValueOrFallback(target, needResult) + if (target is BlockStatement) { + val useInline = target.slotPlan.isEmpty() && target.captureSlots.isEmpty() + return if (useInline) emitInlineBlock(target, needResult) else emitBlock(target, needResult) + } + return compileStatementValueOrFallback(target, needResult) } private fun emitVarDecl(stmt: VarDeclStatement): CompiledValue? { + val scopeId = stmt.scopeId ?: 0 + val scopeSlot = stmt.slotIndex?.let { slotIndex -> + val key = ScopeSlotKey(scopeId, slotIndex) + scopeSlotMap[key] + } ?: run { + if (scopeId == 0) { + scopeSlotIndexByName[stmt.name] + } else { + null + } + } + if (scopeId == 0 && scopeSlot != null) { + val value = stmt.initializer?.let { compileStatementValueOrFallback(it) } ?: run { + builder.emit(Opcode.CONST_NULL, scopeSlot) + updateSlotType(scopeSlot, SlotType.OBJ) + CompiledValue(scopeSlot, SlotType.OBJ) + } + if (value.slot != scopeSlot) { + emitMove(value, scopeSlot) + } + updateSlotType(scopeSlot, value.type) + val declId = builder.addConst( + BytecodeConst.LocalDecl( + stmt.name, + stmt.isMutable, + stmt.visibility, + stmt.isTransient + ) + ) + builder.emit(Opcode.DECL_LOCAL, declId, scopeSlot) + return CompiledValue(scopeSlot, value.type) + } val localSlot = if (allowLocalSlots && stmt.slotIndex != null) { - val key = ScopeSlotKey(stmt.scopeId ?: 0, stmt.slotIndex) + val key = ScopeSlotKey(scopeId, stmt.slotIndex) val localIndex = localSlotIndexByKey[key] localIndex?.let { scopeSlotCount + it } } else { @@ -2152,6 +2252,27 @@ class BytecodeCompiler( builder.emit(Opcode.DECL_LOCAL, declId, localSlot) return CompiledValue(localSlot, value.type) } + if (scopeSlot != null) { + val value = stmt.initializer?.let { compileStatementValueOrFallback(it) } ?: run { + builder.emit(Opcode.CONST_NULL, scopeSlot) + updateSlotType(scopeSlot, SlotType.OBJ) + CompiledValue(scopeSlot, SlotType.OBJ) + } + if (value.slot != scopeSlot) { + emitMove(value, scopeSlot) + } + updateSlotType(scopeSlot, value.type) + val declId = builder.addConst( + BytecodeConst.LocalDecl( + stmt.name, + stmt.isMutable, + stmt.visibility, + stmt.isTransient + ) + ) + builder.emit(Opcode.DECL_LOCAL, declId, scopeSlot) + return CompiledValue(scopeSlot, value.type) + } val value = stmt.initializer?.let { compileStatementValueOrFallback(it) } ?: run { val slot = allocSlot() builder.emit(Opcode.CONST_NULL, slot) @@ -2839,6 +2960,12 @@ class BytecodeCompiler( private fun refPos(ref: BinaryOpRef): Pos = Pos.builtIn private fun resolveSlot(ref: LocalSlotRef): Int? { + val scopeId = refScopeId(ref) + if (scopeId == 0) { + val key = ScopeSlotKey(scopeId, refSlot(ref)) + scopeSlotMap[key]?.let { return it } + scopeSlotIndexByName[ref.name]?.let { return it } + } if (ref.captureOwnerScopeId != null) { val scopeKey = ScopeSlotKey(refScopeId(ref), refSlot(ref)) return scopeSlotMap[scopeKey] diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index 8843c85..e681fe1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -298,7 +298,7 @@ class CmdStoreBoolAddr(internal val src: Int, internal val addrSlot: Int) : Cmd( class CmdIntToReal(internal val src: Int, internal val dst: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { - frame.setReal(dst, frame.getInt(src).toDouble()) + frame.setReal(dst, frame.getReal(src)) return } } @@ -319,7 +319,7 @@ class CmdBoolToInt(internal val src: Int, internal val dst: Int) : Cmd() { class CmdIntToBool(internal val src: Int, internal val dst: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { - frame.setBool(dst, frame.getInt(src) != 0L) + frame.setBool(dst, frame.getBool(src)) return } } @@ -1039,7 +1039,7 @@ class CmdPushScope(internal val planId: Int) : Cmd() { override suspend fun perform(frame: CmdFrame) { val planConst = frame.fn.constants[planId] as? BytecodeConst.SlotPlan ?: error("PUSH_SCOPE expects SlotPlan at $planId") - frame.pushScope(planConst.plan) + frame.pushScope(planConst.plan, planConst.captures) return } } @@ -1504,6 +1504,7 @@ class CmdFrame( internal val scopeVirtualStack = ArrayDeque() internal val slotPlanStack = ArrayDeque>() internal val slotPlanScopeStack = ArrayDeque() + private val captureStack = ArrayDeque>() private var scopeDepth = 0 private var virtualDepth = 0 private val iterStack = ArrayDeque() @@ -1519,7 +1520,11 @@ class CmdFrame( } } - fun pushScope(plan: Map) { + fun pushScope(plan: Map, captures: List) { + val parentScope = scope + if (captures.isNotEmpty() && fn.localSlotNames.isNotEmpty()) { + syncFrameToScope() + } if (scope.skipScopeCreation) { val snapshot = scope.applySlotPlanWithSnapshot(plan) slotPlanStack.addLast(snapshot) @@ -1534,6 +1539,14 @@ class CmdFrame( scope.applySlotPlan(plan) } } + if (captures.isNotEmpty()) { + for (name in captures) { + val rec = parentScope.resolveCaptureRecord(name) + ?: parentScope.raiseSymbolNotFound("symbol ${name} not found") + scope.updateSlotFor(name, rec) + } + } + captureStack.addLast(captures) scopeDepth += 1 } @@ -1548,7 +1561,11 @@ class CmdFrame( } scope = scopeStack.removeLastOrNull() ?: error("Scope stack underflow in POP_SCOPE") + val captures = captureStack.removeLastOrNull() ?: emptyList() scopeDepth -= 1 + if (captures.isNotEmpty() && fn.localSlotNames.isNotEmpty()) { + syncScopeToFrame() + } } fun pushIterator(iter: Obj) { @@ -1613,7 +1630,7 @@ class CmdFrame( fun setObj(slot: Int, value: Obj) { if (slot < fn.scopeSlotCount) { - val target = resolveScope(scope, fn.scopeSlotDepths[slot]) + val target = scope val index = ensureScopeSlot(target, slot) target.setSlotValue(index, value) } else { @@ -1625,7 +1642,14 @@ class CmdFrame( return if (slot < fn.scopeSlotCount) { getScopeSlotValue(slot).toLong() } else { - frame.getInt(slot - fn.scopeSlotCount) + val local = slot - fn.scopeSlotCount + when (frame.getSlotTypeCode(local)) { + SlotType.INT.code -> frame.getInt(local) + SlotType.REAL.code -> frame.getReal(local).toLong() + SlotType.BOOL.code -> if (frame.getBool(local)) 1L else 0L + SlotType.OBJ.code -> frame.getObj(local).toLong() + else -> 0L + } } } @@ -1633,7 +1657,7 @@ class CmdFrame( fun setInt(slot: Int, value: Long) { if (slot < fn.scopeSlotCount) { - val target = resolveScope(scope, fn.scopeSlotDepths[slot]) + val target = scope val index = ensureScopeSlot(target, slot) target.setSlotValue(index, ObjInt.of(value)) } else { @@ -1649,13 +1673,20 @@ class CmdFrame( return if (slot < fn.scopeSlotCount) { getScopeSlotValue(slot).toDouble() } else { - frame.getReal(slot - fn.scopeSlotCount) + val local = slot - fn.scopeSlotCount + when (frame.getSlotTypeCode(local)) { + SlotType.REAL.code -> frame.getReal(local) + SlotType.INT.code -> frame.getInt(local).toDouble() + SlotType.BOOL.code -> if (frame.getBool(local)) 1.0 else 0.0 + SlotType.OBJ.code -> frame.getObj(local).toDouble() + else -> 0.0 + } } } fun setReal(slot: Int, value: Double) { if (slot < fn.scopeSlotCount) { - val target = resolveScope(scope, fn.scopeSlotDepths[slot]) + val target = scope val index = ensureScopeSlot(target, slot) target.setSlotValue(index, ObjReal.of(value)) } else { @@ -1667,7 +1698,14 @@ class CmdFrame( return if (slot < fn.scopeSlotCount) { getScopeSlotValue(slot).toBool() } else { - frame.getBool(slot - fn.scopeSlotCount) + val local = slot - fn.scopeSlotCount + when (frame.getSlotTypeCode(local)) { + SlotType.BOOL.code -> frame.getBool(local) + SlotType.INT.code -> frame.getInt(local) != 0L + SlotType.REAL.code -> frame.getReal(local) != 0.0 + SlotType.OBJ.code -> frame.getObj(local).toBool() + else -> false + } } } @@ -1675,7 +1713,7 @@ class CmdFrame( fun setBool(slot: Int, value: Boolean) { if (slot < fn.scopeSlotCount) { - val target = resolveScope(scope, fn.scopeSlotDepths[slot]) + val target = scope val index = ensureScopeSlot(target, slot) target.setSlotValue(index, if (value) ObjTrue else ObjFalse) } else { @@ -1688,7 +1726,7 @@ class CmdFrame( } fun resolveScopeSlotAddr(scopeSlot: Int, addrSlot: Int) { - val target = resolveScope(scope, fn.scopeSlotDepths[scopeSlot]) + val target = scope val index = ensureScopeSlot(target, scopeSlot) addrScopes[addrSlot] = target addrIndices[addrSlot] = index @@ -1877,10 +1915,7 @@ class CmdFrame( } private fun resolveLocalScope(localIndex: Int): Scope? { - val depth = fn.localSlotDepths.getOrNull(localIndex) ?: return scope - val relativeDepth = scopeDepth - depth - if (relativeDepth < 0) return null - return if (relativeDepth == 0) scope else resolveScope(scope, relativeDepth) + return scope } private fun localSlotToObj(localIndex: Int): Obj { @@ -1894,7 +1929,7 @@ class CmdFrame( } private fun getScopeSlotValue(slot: Int): Obj { - val target = resolveScope(scope, fn.scopeSlotDepths[slot]) + val target = scope val index = ensureScopeSlot(target, slot) val record = target.getSlotRecord(index) if (record.value !== ObjUnset) return record.value @@ -1933,8 +1968,10 @@ class CmdFrame( if (existing != null) return existing } val index = fn.scopeSlotIndices[slot] - if (index < target.slotCount) return index - if (name == null) return index + if (name == null) { + if (index < target.slotCount) return index + return index + } target.applySlotPlan(mapOf(name to index)) val existing = target.getLocalRecordDirect(name) if (existing != null) { @@ -1948,18 +1985,5 @@ class CmdFrame( return index } - private fun resolveScope(start: Scope, depth: Int): Scope { - if (depth == 0) return start - var effectiveDepth = depth - if (virtualDepth > 0) { - if (effectiveDepth <= virtualDepth) return start - effectiveDepth -= virtualDepth - } - val next = when (start) { - is net.sergeych.lyng.ClosureScope -> start.closureScope - else -> start.parent - } - return next?.let { resolveScope(it, effectiveDepth - 1) } - ?: error("Scope depth $depth is out of range") - } + // Scope depth resolution is no longer used; all scope slots are resolved against the current frame. } diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 15f2fb2..c81451e 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -652,7 +652,6 @@ class ScriptTest { } - @Ignore("incremental enable") @Test fun whileAssignTest() = runTest { eval( @@ -665,7 +664,6 @@ class ScriptTest { ) } - @Ignore("incremental enable") @Test fun whileTest() = runTest { assertEquals( @@ -720,7 +718,6 @@ class ScriptTest { ) } - @Ignore("incremental enable") @Test fun testAssignArgumentsNoEllipsis() = runTest { // equal args, no ellipsis, no defaults, ok @@ -762,7 +759,7 @@ class ScriptTest { assertEquals(ObjInt(5), c["c"]?.value) } - @Ignore("incremental enable") + @Ignore("Scope.eval should seed compile-time symbols from current scope") @Test fun testAssignArgumentsEndEllipsis() = runTest { // equal args, @@ -864,7 +861,6 @@ class ScriptTest { } } - @Ignore("incremental enable") @Test fun testWhileBlockIsolation1() = runTest { eval( @@ -881,7 +877,6 @@ class ScriptTest { ) } - @Ignore("incremental enable") @Test fun testWhileBlockIsolation2() = runTest { assertFails { @@ -924,7 +919,7 @@ class ScriptTest { ) } - @Ignore("incremental enable") + @Ignore("bytecode fallback in labeled break") @Test fun whileNonLocalBreakTest() = runTest { assertEquals(