From 144082733c7d69908c881fbf42ddf99e7e2eca75 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 26 Jan 2026 05:47:37 +0300 Subject: [PATCH] Expand bytecode expressions and loops --- docs/BytecodeSpec.md | 9 + .../kotlin/net/sergeych/lyng/Compiler.kt | 207 +----------- .../sergeych/lyng/bytecode/BytecodeBuilder.kt | 4 +- .../lyng/bytecode/BytecodeCompiler.kt | 304 ++++++++++++++++++ .../sergeych/lyng/bytecode/BytecodeConst.kt | 1 + .../lyng/bytecode/BytecodeDisassembler.kt | 4 +- .../net/sergeych/lyng/bytecode/BytecodeVm.kt | 19 +- .../net/sergeych/lyng/bytecode/Opcode.kt | 2 + .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 20 +- .../kotlin/net/sergeych/lyng/statements.kt | 218 +++++++++++++ notes/bytecode_exprs_loops.md | 11 + 11 files changed, 590 insertions(+), 209 deletions(-) create mode 100644 notes/bytecode_exprs_loops.md diff --git a/docs/BytecodeSpec.md b/docs/BytecodeSpec.md index 86e136e..fd75bbd 100644 --- a/docs/BytecodeSpec.md +++ b/docs/BytecodeSpec.md @@ -30,6 +30,9 @@ slots[localCount .. localCount+argCount-1] arguments - scopeSlotNames: array sized scopeSlotCount, each entry nullable. - Intended for disassembly/debug tooling; VM semantics do not depend on it. +### Constant pool extras +- SlotPlan: map of name -> slot index, used by PUSH_SCOPE to pre-allocate and map loop locals. + ## 2) Slot ID Width Per frame, select: @@ -185,6 +188,12 @@ Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass. - JMP_IF_FALSE S, I - RET S - RET_VOID +- PUSH_SCOPE K +- POP_SCOPE + +### Scope setup +- PUSH_SCOPE uses const `SlotPlan` (name -> slot index) to create a child scope and apply slot mapping. +- POP_SCOPE restores the parent scope. ### Calls - CALL_DIRECT F, S, C, S diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 975867d..b4ceee8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -2556,174 +2556,23 @@ class Compiler( } val loopSlotPlanSnapshot = slotPlanIndices(loopSlotPlan) - return object : Statement() { - override val pos: Pos = body.pos - override suspend fun execute(scope: Scope): Obj { - val forContext = scope.createChildScope(start) - if (loopSlotPlanSnapshot.isNotEmpty()) { - forContext.applySlotPlan(loopSlotPlanSnapshot) - } - - // loop var: StoredObject - val loopSO = forContext.addItem(tVar.value, true, ObjNull) - - if (constRange != null && PerfFlags.PRIMITIVE_FASTOPS) { - val loopSlotIndex = forContext.getSlotIndexOf(tVar.value) ?: -1 - return loopIntRange( - forContext, - constRange.start, - constRange.endExclusive, - loopSO, - loopSlotIndex, - body, - elseStatement, - label, - canBreak - ) - } - // insofar we suggest source object is enumerable. Later we might need to add checks - val sourceObj = source.execute(forContext) - - if (sourceObj is ObjRange && sourceObj.isIntRange && PerfFlags.PRIMITIVE_FASTOPS) { - val loopSlotIndex = forContext.getSlotIndexOf(tVar.value) ?: -1 - return loopIntRange( - forContext, - sourceObj.start!!.toLong(), - if (sourceObj.isEndInclusive) - sourceObj.end!!.toLong() + 1 - else - sourceObj.end!!.toLong(), - loopSO, - loopSlotIndex, - body, - elseStatement, - label, - canBreak - ) - } else if (sourceObj.isInstanceOf(ObjIterable)) { - return loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak) - } else { - val size = runCatching { sourceObj.readField(forContext, "size").value.toInt() } - .getOrElse { - throw ScriptError( - tOp.pos, - "object is not enumerable: no size in $sourceObj", - it - ) - } - - var result: Obj = ObjVoid - var breakCaught = false - - if (size > 0) { - var current = runCatching { sourceObj.getAt(forContext, ObjInt.of(0)) } - .getOrElse { - throw ScriptError( - tOp.pos, - "object is not enumerable: no index access for ${sourceObj.inspect(scope)}", - it - ) - } - var index = 0 - while (true) { - loopSO.value = current - try { - result = body.execute(forContext) - } catch (lbe: LoopBreakContinueException) { - if (lbe.label == label || lbe.label == null) { - breakCaught = true - if (lbe.doContinue) continue - else { - result = lbe.result - break - } - } else - throw lbe - } - if (++index >= size) break - current = sourceObj.getAt(forContext, ObjInt.of(index.toLong())) - } - } - if (!breakCaught && elseStatement != null) { - result = elseStatement.execute(scope) - } - return result - } - } - } + return ForInStatement( + loopVarName = tVar.value, + source = source, + constRange = constRange, + body = body, + elseStatement = elseStatement, + label = label, + canBreak = canBreak, + loopSlotPlan = loopSlotPlanSnapshot, + pos = body.pos + ) } else { // maybe other loops? throw ScriptError(tOp.pos, "Unsupported for-loop syntax") } } - private suspend fun loopIntRange( - forScope: Scope, start: Long, end: Long, loopVar: ObjRecord, loopSlotIndex: Int, - body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean - ): Obj { - var result: Obj = ObjVoid - val cacheLow = ObjInt.CACHE_LOW - val cacheHigh = ObjInt.CACHE_HIGH - val useCache = start >= cacheLow && end <= cacheHigh + 1 - val cache = if (useCache) ObjInt.cacheArray() else null - val useSlot = loopSlotIndex >= 0 - if (catchBreak) { - if (useCache && cache != null) { - var i = start - while (i < end) { - val v = cache[(i - cacheLow).toInt()] - if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v - try { - result = body.execute(forScope) - } catch (lbe: LoopBreakContinueException) { - if (lbe.label == label || lbe.label == null) { - if (lbe.doContinue) { - i++ - continue - } - return lbe.result - } - throw lbe - } - i++ - } - } else { - for (i in start.. - loopVar.value = item - if (catchBreak) { - try { - result = body.execute(forScope) - true - } catch (lbe: LoopBreakContinueException) { - if (lbe.label == label || lbe.label == null) { - if (lbe.doContinue) true - else { - result = lbe.result - breakCaught = true - false - } - } else - throw lbe - } - } else { - result = body.execute(forScope) - true - } - } - return if (!breakCaught && elseStatement != null) { - elseStatement.execute(forScope) - } else result - } - @Suppress("UNUSED_VARIABLE") private suspend fun parseDoWhileStatement(): Statement { val label = getLabel()?.also { cc.labels += it } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt index b966375..6740239 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt @@ -129,7 +129,7 @@ class BytecodeBuilder { private fun operandKinds(op: Opcode): List { return when (op) { - Opcode.NOP, Opcode.RET_VOID -> emptyList() + Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE -> emptyList() Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ, Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL, Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT -> @@ -138,6 +138,8 @@ class BytecodeBuilder { listOf(OperandKind.SLOT) Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL -> listOf(OperandKind.CONST, OperandKind.SLOT) + Opcode.PUSH_SCOPE -> + listOf(OperandKind.CONST) Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT, Opcode.ADD_REAL, Opcode.SUB_REAL, Opcode.MUL_REAL, Opcode.DIV_REAL, Opcode.AND_INT, Opcode.OR_INT, Opcode.XOR_INT, Opcode.SHL_INT, Opcode.SHR_INT, Opcode.USHR_INT, 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 4efd5f2..20ce3f1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -42,6 +42,7 @@ class BytecodeCompiler( return when (stmt) { is ExpressionStatement -> compileExpression(name, stmt) is net.sergeych.lyng.IfStatement -> compileIf(name, stmt) + is net.sergeych.lyng.ForInStatement -> compileForIn(name, stmt) else -> null } } @@ -71,6 +72,10 @@ class BytecodeCompiler( is BinaryOpRef -> compileBinary(ref) is UnaryOpRef -> compileUnary(ref) is AssignRef -> compileAssign(ref) + is AssignOpRef -> compileAssignOp(ref) + is IncDecRef -> compileIncDec(ref) + is ConditionalRef -> compileConditional(ref) + is ElvisRef -> compileElvis(ref) is CallRef -> compileCall(ref) is MethodCallRef -> compileMethodCall(ref) else -> null @@ -590,6 +595,173 @@ class BytecodeCompiler( return CompiledValue(slot, value.type) } + private fun compileAssignOp(ref: AssignOpRef): CompiledValue? { + val target = ref.target as? LocalSlotRef ?: return null + if (!allowLocalSlots) return null + if (!target.isMutable || target.isDelegated) return null + if (refDepth(target) > 0) return null + val slot = scopeSlotMap[ScopeSlotKey(refDepth(target), refSlot(target))] ?: return null + val targetType = slotTypes[slot] ?: return null + val rhs = compileRef(ref.value) ?: return null + val out = slot + val result = when (ref.op) { + BinOp.PLUS -> compileAssignOpBinary(targetType, rhs, out, Opcode.ADD_INT, Opcode.ADD_REAL, Opcode.ADD_OBJ) + BinOp.MINUS -> compileAssignOpBinary(targetType, rhs, out, Opcode.SUB_INT, Opcode.SUB_REAL, Opcode.SUB_OBJ) + BinOp.STAR -> compileAssignOpBinary(targetType, rhs, out, Opcode.MUL_INT, Opcode.MUL_REAL, Opcode.MUL_OBJ) + BinOp.SLASH -> compileAssignOpBinary(targetType, rhs, out, Opcode.DIV_INT, Opcode.DIV_REAL, Opcode.DIV_OBJ) + BinOp.PERCENT -> compileAssignOpBinary(targetType, rhs, out, Opcode.MOD_INT, null, Opcode.MOD_OBJ) + else -> null + } ?: return null + updateSlotType(out, result.type) + return CompiledValue(out, result.type) + } + + private fun compileAssignOpBinary( + targetType: SlotType, + rhs: CompiledValue, + out: Int, + intOp: Opcode, + realOp: Opcode?, + objOp: Opcode?, + ): CompiledValue? { + return when (targetType) { + SlotType.INT -> { + when (rhs.type) { + SlotType.INT -> { + builder.emit(intOp, out, rhs.slot, out) + CompiledValue(out, SlotType.INT) + } + SlotType.REAL -> { + if (realOp == null) return null + val left = allocSlot() + builder.emit(Opcode.INT_TO_REAL, out, left) + builder.emit(realOp, left, rhs.slot, out) + CompiledValue(out, SlotType.REAL) + } + else -> null + } + } + SlotType.REAL -> { + if (realOp == null) return null + when (rhs.type) { + SlotType.REAL -> { + builder.emit(realOp, out, rhs.slot, out) + CompiledValue(out, SlotType.REAL) + } + SlotType.INT -> { + val right = allocSlot() + builder.emit(Opcode.INT_TO_REAL, rhs.slot, right) + builder.emit(realOp, out, right, out) + CompiledValue(out, SlotType.REAL) + } + else -> null + } + } + SlotType.OBJ -> { + if (objOp == null) return null + if (rhs.type != SlotType.OBJ) return null + builder.emit(objOp, out, rhs.slot, out) + CompiledValue(out, SlotType.OBJ) + } + else -> null + } + } + + private fun compileIncDec(ref: IncDecRef): CompiledValue? { + val target = ref.target as? LocalSlotRef ?: return null + if (!allowLocalSlots) return null + if (!target.isMutable || target.isDelegated) return null + if (refDepth(target) > 0) return null + val slot = scopeSlotMap[ScopeSlotKey(refDepth(target), refSlot(target))] ?: return null + val slotType = slotTypes[slot] ?: return null + return when (slotType) { + SlotType.INT -> { + if (ref.isPost) { + val old = allocSlot() + builder.emit(Opcode.MOVE_INT, slot, old) + builder.emit(if (ref.isIncrement) Opcode.INC_INT else Opcode.DEC_INT, slot) + CompiledValue(old, SlotType.INT) + } else { + builder.emit(if (ref.isIncrement) Opcode.INC_INT else Opcode.DEC_INT, slot) + CompiledValue(slot, SlotType.INT) + } + } + SlotType.REAL -> { + val oneSlot = allocSlot() + val oneId = builder.addConst(BytecodeConst.RealVal(1.0)) + builder.emit(Opcode.CONST_REAL, oneId, oneSlot) + if (ref.isPost) { + val old = allocSlot() + builder.emit(Opcode.MOVE_REAL, slot, old) + val op = if (ref.isIncrement) Opcode.ADD_REAL else Opcode.SUB_REAL + builder.emit(op, slot, oneSlot, slot) + CompiledValue(old, SlotType.REAL) + } else { + val op = if (ref.isIncrement) Opcode.ADD_REAL else Opcode.SUB_REAL + builder.emit(op, slot, oneSlot, slot) + CompiledValue(slot, SlotType.REAL) + } + } + else -> null + } + } + + private fun compileConditional(ref: ConditionalRef): CompiledValue? { + val condition = compileRefWithFallback(ref.condition, SlotType.BOOL, Pos.builtIn) ?: return null + if (condition.type != SlotType.BOOL) return null + val resultSlot = allocSlot() + val elseLabel = builder.label() + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_FALSE, + listOf(BytecodeBuilder.Operand.IntVal(condition.slot), BytecodeBuilder.Operand.LabelRef(elseLabel)) + ) + val thenValue = compileRefWithFallback(ref.ifTrue, null, Pos.builtIn) ?: return null + val thenObj = ensureObjSlot(thenValue) + builder.emit(Opcode.MOVE_OBJ, thenObj.slot, resultSlot) + builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel))) + builder.mark(elseLabel) + val elseValue = compileRefWithFallback(ref.ifFalse, null, Pos.builtIn) ?: return null + val elseObj = ensureObjSlot(elseValue) + builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot) + builder.mark(endLabel) + updateSlotType(resultSlot, SlotType.OBJ) + return CompiledValue(resultSlot, SlotType.OBJ) + } + + private fun compileElvis(ref: ElvisRef): CompiledValue? { + val leftValue = compileRefWithFallback(ref.left, null, Pos.builtIn) ?: return null + val leftObj = ensureObjSlot(leftValue) + val resultSlot = allocSlot() + val nullSlot = allocSlot() + builder.emit(Opcode.CONST_NULL, nullSlot) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_REF_EQ_OBJ, leftObj.slot, nullSlot, cmpSlot) + val rightLabel = builder.label() + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(BytecodeBuilder.Operand.IntVal(cmpSlot), BytecodeBuilder.Operand.LabelRef(rightLabel)) + ) + builder.emit(Opcode.MOVE_OBJ, leftObj.slot, resultSlot) + builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel))) + builder.mark(rightLabel) + val rightValue = compileRefWithFallback(ref.right, null, Pos.builtIn) ?: return null + val rightObj = ensureObjSlot(rightValue) + builder.emit(Opcode.MOVE_OBJ, rightObj.slot, resultSlot) + builder.mark(endLabel) + updateSlotType(resultSlot, SlotType.OBJ) + return CompiledValue(resultSlot, SlotType.OBJ) + } + + private fun ensureObjSlot(value: CompiledValue): CompiledValue { + if (value.type == SlotType.OBJ) return value + val dst = allocSlot() + builder.emit(Opcode.BOX_OBJ, value.slot, dst) + updateSlotType(dst, SlotType.OBJ) + return CompiledValue(dst, SlotType.OBJ) + } + private data class CallArgs(val base: Int, val count: Int) private fun compileCall(ref: CallRef): CompiledValue? { @@ -680,6 +852,54 @@ class BytecodeCompiler( return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames) } + private fun compileForIn(name: String, stmt: net.sergeych.lyng.ForInStatement): BytecodeFunction? { + if (stmt.canBreak) return null + val range = stmt.constRange ?: return null + val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName] ?: return null + val loopSlot = scopeSlotMap[ScopeSlotKey(0, loopSlotIndex)] ?: return null + val planId = builder.addConst(BytecodeConst.SlotPlan(stmt.loopSlotPlan)) + builder.emit(Opcode.PUSH_SCOPE, planId) + + val iSlot = allocSlot() + val endSlot = allocSlot() + val startId = builder.addConst(BytecodeConst.IntVal(range.start)) + val endId = builder.addConst(BytecodeConst.IntVal(range.endExclusive)) + builder.emit(Opcode.CONST_INT, startId, iSlot) + builder.emit(Opcode.CONST_INT, endId, endSlot) + + val resultSlot = allocSlot() + val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + builder.emit(Opcode.CONST_OBJ, voidId, resultSlot) + + val loopLabel = builder.label() + val endLabel = builder.label() + builder.mark(loopLabel) + val cmpSlot = allocSlot() + builder.emit(Opcode.CMP_GTE_INT, iSlot, endSlot, cmpSlot) + builder.emit( + Opcode.JMP_IF_TRUE, + listOf(BytecodeBuilder.Operand.IntVal(cmpSlot), BytecodeBuilder.Operand.LabelRef(endLabel)) + ) + builder.emit(Opcode.MOVE_INT, iSlot, loopSlot) + val bodyValue = compileStatementValueOrFallback(stmt.body) ?: return null + val bodyObj = ensureObjSlot(bodyValue) + builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot) + builder.emit(Opcode.INC_INT, iSlot) + builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(loopLabel))) + + builder.mark(endLabel) + if (stmt.elseStatement != null) { + val elseValue = compileStatementValueOrFallback(stmt.elseStatement) ?: return null + val elseObj = ensureObjSlot(elseValue) + builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot) + } + builder.emit(Opcode.POP_SCOPE) + builder.emit(Opcode.RET, resultSlot) + + val localCount = maxOf(nextSlot, resultSlot + 1) - scopeSlotCount + return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames) + } + private fun compileStatementValue(stmt: Statement): CompiledValue? { return when (stmt) { is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos) @@ -687,6 +907,61 @@ class BytecodeCompiler( } } + private fun compileStatementValueOrFallback(stmt: Statement): CompiledValue? { + return when (stmt) { + is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos) + is IfStatement -> compileIfExpression(stmt) + else -> { + val slot = allocSlot() + val id = builder.addFallback(stmt) + builder.emit(Opcode.EVAL_FALLBACK, id, slot) + updateSlotType(slot, SlotType.OBJ) + CompiledValue(slot, SlotType.OBJ) + } + } + } + + private fun compileIfExpression(stmt: IfStatement): CompiledValue? { + val condition = compileCondition(stmt.condition, stmt.pos) ?: return null + if (condition.type != SlotType.BOOL) return null + val resultSlot = allocSlot() + val elseLabel = builder.label() + val endLabel = builder.label() + builder.emit( + Opcode.JMP_IF_FALSE, + listOf(BytecodeBuilder.Operand.IntVal(condition.slot), BytecodeBuilder.Operand.LabelRef(elseLabel)) + ) + val thenValue = compileStatementValueOrFallback(stmt.ifBody) ?: return null + val thenObj = ensureObjSlot(thenValue) + builder.emit(Opcode.MOVE_OBJ, thenObj.slot, resultSlot) + builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel))) + builder.mark(elseLabel) + if (stmt.elseBody != null) { + val elseValue = compileStatementValueOrFallback(stmt.elseBody) ?: return null + val elseObj = ensureObjSlot(elseValue) + builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot) + } else { + val id = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + builder.emit(Opcode.CONST_OBJ, id, resultSlot) + } + builder.mark(endLabel) + updateSlotType(resultSlot, SlotType.OBJ) + return CompiledValue(resultSlot, SlotType.OBJ) + } + + private fun compileCondition(stmt: Statement, pos: Pos): CompiledValue? { + return when (stmt) { + is ExpressionStatement -> compileRefWithFallback(stmt.ref, SlotType.BOOL, stmt.pos) + else -> { + val slot = allocSlot() + val id = builder.addFallback(ToBoolStatement(stmt, pos)) + builder.emit(Opcode.EVAL_FALLBACK, id, slot) + updateSlotType(slot, SlotType.BOOL) + CompiledValue(slot, SlotType.BOOL) + } + } + } + private fun emitMove(value: CompiledValue, dstSlot: Int) { when (value.type) { SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, dstSlot) @@ -764,6 +1039,21 @@ class BytecodeCompiler( collectScopeSlots(stmt.ifBody) stmt.elseBody?.let { collectScopeSlots(it) } } + is net.sergeych.lyng.ForInStatement -> { + val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName] + if (loopSlotIndex != null) { + val key = ScopeSlotKey(0, loopSlotIndex) + if (!scopeSlotMap.containsKey(key)) { + scopeSlotMap[key] = scopeSlotMap.size + } + if (!scopeSlotNameMap.containsKey(key)) { + scopeSlotNameMap[key] = stmt.loopVarName + } + } + collectScopeSlots(stmt.source) + collectScopeSlots(stmt.body) + stmt.elseStatement?.let { collectScopeSlots(it) } + } else -> {} } } @@ -797,6 +1087,20 @@ class BytecodeCompiler( } collectScopeSlotsRef(assignValue(ref)) } + is AssignOpRef -> { + collectScopeSlotsRef(ref.target) + collectScopeSlotsRef(ref.value) + } + is IncDecRef -> collectScopeSlotsRef(ref.target) + is ConditionalRef -> { + collectScopeSlotsRef(ref.condition) + collectScopeSlotsRef(ref.ifTrue) + collectScopeSlotsRef(ref.ifFalse) + } + is ElvisRef -> { + collectScopeSlotsRef(ref.left) + collectScopeSlotsRef(ref.right) + } is CallRef -> { collectScopeSlotsRef(ref.target) collectScopeSlotsArgs(ref.args) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt index c227839..0394918 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt @@ -25,4 +25,5 @@ sealed class BytecodeConst { data class RealVal(val value: Double) : BytecodeConst() data class StringVal(val value: String) : BytecodeConst() data class ObjRef(val value: Obj) : BytecodeConst() + data class SlotPlan(val plan: Map) : BytecodeConst() } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt index 995c280..b66ae11 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeDisassembler.kt @@ -82,7 +82,7 @@ object BytecodeDisassembler { private fun operandKinds(op: Opcode): List { return when (op) { - Opcode.NOP, Opcode.RET_VOID -> emptyList() + Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE -> emptyList() Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ, Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL, Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT -> @@ -91,6 +91,8 @@ object BytecodeDisassembler { listOf(OperandKind.SLOT) Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL -> listOf(OperandKind.CONST, OperandKind.SLOT) + Opcode.PUSH_SCOPE -> + listOf(OperandKind.CONST) Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT, Opcode.ADD_REAL, Opcode.SUB_REAL, Opcode.MUL_REAL, Opcode.DIV_REAL, Opcode.AND_INT, Opcode.OR_INT, Opcode.XOR_INT, Opcode.SHL_INT, Opcode.SHR_INT, Opcode.USHR_INT, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt index a046a95..4f33095 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt @@ -22,7 +22,9 @@ import net.sergeych.lyng.Scope import net.sergeych.lyng.obj.* class BytecodeVm { - suspend fun execute(fn: BytecodeFunction, scope: Scope, args: List): Obj { + suspend fun execute(fn: BytecodeFunction, scope0: Scope, args: List): Obj { + val scopeStack = ArrayDeque() + var scope = scope0 val frame = BytecodeFrame(fn.localCount, args.size) for (i in args.indices) { frame.setObj(frame.argBase + i, args[i]) @@ -721,6 +723,21 @@ class BytecodeVm { ip = target } } + Opcode.PUSH_SCOPE -> { + val constId = decoder.readConstId(code, ip, fn.constIdWidth) + ip += fn.constIdWidth + val planConst = fn.constants[constId] as? BytecodeConst.SlotPlan + ?: error("PUSH_SCOPE expects SlotPlan at $constId") + scopeStack.addLast(scope) + scope = scope.createChildScope() + if (planConst.plan.isNotEmpty()) { + scope.applySlotPlan(planConst.plan) + } + } + Opcode.POP_SCOPE -> { + scope = scopeStack.removeLastOrNull() + ?: error("Scope stack underflow in POP_SCOPE") + } Opcode.CALL_SLOT -> { val calleeSlot = decoder.readSlot(code, ip) ip += fn.slotWidth diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt index d3de9cb..008e2f4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -107,6 +107,8 @@ enum class Opcode(val code: Int) { JMP_IF_FALSE(0x82), RET(0x83), RET_VOID(0x84), + PUSH_SCOPE(0x85), + POP_SCOPE(0x86), CALL_DIRECT(0x90), CALL_VIRTUAL(0x91), diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index d301a34..53a53b8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -385,9 +385,9 @@ class BinaryOpRef(internal val op: BinOp, internal val left: ObjRef, internal va /** Conditional (ternary) operator reference: cond ? a : b */ class ConditionalRef( - private val condition: ObjRef, - private val ifTrue: ObjRef, - private val ifFalse: ObjRef + internal val condition: ObjRef, + internal val ifTrue: ObjRef, + internal val ifFalse: ObjRef ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { return evalCondition(scope).get(scope) @@ -661,9 +661,9 @@ class QualifiedThisMethodSlotCallRef( /** Assignment compound op: target op= value */ class AssignOpRef( - private val op: BinOp, - private val target: ObjRef, - private val value: ObjRef, + internal val op: BinOp, + internal val target: ObjRef, + internal val value: ObjRef, private val atPos: Pos, ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { @@ -723,9 +723,9 @@ class AssignOpRef( /** Pre/post ++/-- on l-values */ class IncDecRef( - private val target: ObjRef, - private val isIncrement: Boolean, - private val isPost: Boolean, + internal val target: ObjRef, + internal val isIncrement: Boolean, + internal val isPost: Boolean, private val atPos: Pos, ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { @@ -751,7 +751,7 @@ class IncDecRef( } /** Elvis operator reference: a ?: b */ -class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { +class ElvisRef(internal val left: ObjRef, internal val right: ObjRef) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { val a = left.evalValue(scope) val r = if (a != ObjNull) a else right.evalValue(scope) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt index 157182a..d0f0381 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt @@ -19,8 +19,15 @@ package net.sergeych.lyng import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.ObjClass +import net.sergeych.lyng.obj.ObjInt +import net.sergeych.lyng.obj.ObjIterable +import net.sergeych.lyng.obj.ObjNull +import net.sergeych.lyng.obj.ObjRange +import net.sergeych.lyng.obj.ObjRecord import net.sergeych.lyng.obj.ObjVoid import net.sergeych.lyng.obj.toBool +import net.sergeych.lyng.obj.toInt +import net.sergeych.lyng.obj.toLong fun String.toSource(name: String = "eval"): Source = Source(name, this) @@ -79,6 +86,217 @@ class IfStatement( } } +data class ConstIntRange(val start: Long, val endExclusive: Long) + +class ForInStatement( + val loopVarName: String, + val source: Statement, + val constRange: ConstIntRange?, + val body: Statement, + val elseStatement: Statement?, + val label: String?, + val canBreak: Boolean, + val loopSlotPlan: Map, + override val pos: Pos, +) : Statement() { + override suspend fun execute(scope: Scope): Obj { + val forContext = scope.createChildScope(pos) + if (loopSlotPlan.isNotEmpty()) { + forContext.applySlotPlan(loopSlotPlan) + } + + val loopSO = forContext.addItem(loopVarName, true, ObjNull) + val loopSlotIndex = forContext.getSlotIndexOf(loopVarName) ?: -1 + + if (constRange != null && PerfFlags.PRIMITIVE_FASTOPS) { + return loopIntRange( + forContext, + constRange.start, + constRange.endExclusive, + loopSO, + loopSlotIndex, + body, + elseStatement, + label, + canBreak + ) + } + + val sourceObj = source.execute(forContext) + return if (sourceObj is ObjRange && sourceObj.isIntRange && PerfFlags.PRIMITIVE_FASTOPS) { + loopIntRange( + forContext, + sourceObj.start!!.toLong(), + if (sourceObj.isEndInclusive) sourceObj.end!!.toLong() + 1 else sourceObj.end!!.toLong(), + loopSO, + loopSlotIndex, + body, + elseStatement, + label, + canBreak + ) + } else if (sourceObj.isInstanceOf(ObjIterable)) { + loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak) + } else { + val size = runCatching { sourceObj.readField(forContext, "size").value.toInt() } + .getOrElse { + throw ScriptError( + pos, + "object is not enumerable: no size in $sourceObj", + it + ) + } + + var result: Obj = ObjVoid + var breakCaught = false + + if (size > 0) { + var current = runCatching { sourceObj.getAt(forContext, ObjInt.of(0)) } + .getOrElse { + throw ScriptError( + pos, + "object is not enumerable: no index access for ${sourceObj.inspect(scope)}", + it + ) + } + var index = 0 + while (true) { + loopSO.value = current + try { + result = body.execute(forContext) + } catch (lbe: LoopBreakContinueException) { + if (lbe.label == label || lbe.label == null) { + breakCaught = true + if (lbe.doContinue) continue + result = lbe.result + break + } else { + throw lbe + } + } + if (++index >= size) break + current = sourceObj.getAt(forContext, ObjInt.of(index.toLong())) + } + } + if (!breakCaught && elseStatement != null) { + result = elseStatement.execute(scope) + } + result + } + } + + private suspend fun loopIntRange( + forScope: Scope, + start: Long, + end: Long, + loopVar: ObjRecord, + loopSlotIndex: Int, + body: Statement, + elseStatement: Statement?, + label: String?, + catchBreak: Boolean, + ): Obj { + var result: Obj = ObjVoid + val cacheLow = ObjInt.CACHE_LOW + val cacheHigh = ObjInt.CACHE_HIGH + val useCache = start >= cacheLow && end <= cacheHigh + 1 + val cache = if (useCache) ObjInt.cacheArray() else null + val useSlot = loopSlotIndex >= 0 + if (catchBreak) { + if (useCache && cache != null) { + var i = start + while (i < end) { + val v = cache[(i - cacheLow).toInt()] + if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v + try { + result = body.execute(forScope) + } catch (lbe: LoopBreakContinueException) { + if (lbe.label == label || lbe.label == null) { + if (lbe.doContinue) { + i++ + continue + } + return lbe.result + } + throw lbe + } + i++ + } + } else { + for (i in start.. + loopVar.value = item + if (catchBreak) { + try { + result = body.execute(forScope) + true + } catch (lbe: LoopBreakContinueException) { + if (lbe.label == label || lbe.label == null) { + breakCaught = true + if (lbe.doContinue) true else { + result = lbe.result + false + } + } else { + throw lbe + } + } + } else { + result = body.execute(forScope) + true + } + } + if (!breakCaught && elseStatement != null) { + result = elseStatement.execute(forScope) + } + return result + } +} + class ToBoolStatement( val expr: Statement, override val pos: Pos, diff --git a/notes/bytecode_exprs_loops.md b/notes/bytecode_exprs_loops.md new file mode 100644 index 0000000..12f2808 --- /dev/null +++ b/notes/bytecode_exprs_loops.md @@ -0,0 +1,11 @@ +# Bytecode expression + for-in loop support + +Changes +- Added bytecode compilation for conditional/elvis expressions, inc/dec, and compound assignments where safe. +- Added ForInStatement and ConstIntRange to keep for-loop structure explicit (no anonymous Statement). +- Added PUSH_SCOPE/POP_SCOPE opcodes with SlotPlan constants to create loop scopes in bytecode. +- Bytecode compiler emits int-range for-in loops when const range is known and no break/continue. + +Tests +- ./gradlew :lynglib:jvmTest +- ./gradlew :lynglib:allTests -x :lynglib:jvmTest