From 6560457e3d6f8a4e7a5b25a522be7b331528bff4 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 25 Jan 2026 19:05:42 +0300 Subject: [PATCH] Add bytecode if support and test --- .../kotlin/net/sergeych/lyng/Compiler.kt | 19 +--- .../sergeych/lyng/bytecode/BytecodeBuilder.kt | 54 +++++++++++- .../lyng/bytecode/BytecodeCompiler.kt | 87 ++++++++++++++----- .../net/sergeych/lyng/bytecode/BytecodeVm.kt | 31 +++++++ .../kotlin/net/sergeych/lyng/statements.kt | 16 ++++ .../src/commonTest/kotlin/BytecodeVmTest.kt | 31 +++++++ 6 files changed, 195 insertions(+), 43 deletions(-) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index a3e858a..cf812e1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -2965,25 +2965,10 @@ class Compiler( return if (t2.type == Token.Type.ID && t2.value == "else") { val elseBody = parseStatement() ?: throw ScriptError(pos, "Bad else statement: expected statement") - return object : Statement() { - override val pos: Pos = start - override suspend fun execute(scope: Scope): Obj { - return if (condition.execute(scope).toBool()) - ifBody.execute(scope) - else - elseBody.execute(scope) - } - } + IfStatement(condition, ifBody, elseBody, start) } else { cc.previous() - object : Statement() { - override val pos: Pos = start - override suspend fun execute(scope: Scope): Obj { - if (condition.execute(scope).toBool()) - return ifBody.execute(scope) - return ObjVoid - } - } + IfStatement(condition, ifBody, null, start) } } 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 6fa7486..bd93c40 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt @@ -17,10 +17,19 @@ package net.sergeych.lyng.bytecode class BytecodeBuilder { - data class Instr(val op: Opcode, val operands: IntArray) + sealed interface Operand { + data class IntVal(val value: Int) : Operand + data class LabelRef(val label: Label) : Operand + } + + data class Label(val id: Int) + + data class Instr(val op: Opcode, val operands: List) private val instructions = mutableListOf() private val constPool = mutableListOf() + private val labelPositions = mutableMapOf() + private var nextLabelId = 0 fun addConst(c: BytecodeConst): Int { constPool += c @@ -28,7 +37,17 @@ class BytecodeBuilder { } fun emit(op: Opcode, vararg operands: Int) { - instructions += Instr(op, operands.copyOf()) + instructions += Instr(op, operands.map { Operand.IntVal(it) }) + } + + fun emit(op: Opcode, operands: List) { + instructions += Instr(op, operands) + } + + fun label(): Label = Label(nextLabelId++) + + fun mark(label: Label) { + labelPositions[label] = instructions.size } fun build(name: String, localCount: Int): BytecodeFunction { @@ -39,15 +58,32 @@ class BytecodeBuilder { } val constIdWidth = if (constPool.size < 65536) 2 else 4 val ipWidth = 2 + val instrOffsets = IntArray(instructions.size) + var currentIp = 0 + for (i in instructions.indices) { + instrOffsets[i] = currentIp + val kinds = operandKinds(instructions[i].op) + currentIp += 1 + kinds.sumOf { operandWidth(it, slotWidth, constIdWidth, ipWidth) } + } + val labelIps = mutableMapOf() + for ((label, idx) in labelPositions) { + labelIps[label] = instrOffsets.getOrNull(idx) ?: error("Invalid label index: $idx") + } + val code = ByteArrayOutput() for (ins in instructions) { - code.writeU8(ins.op.code.toInt() and 0xFF) + code.writeU8(ins.op.code and 0xFF) val kinds = operandKinds(ins.op) if (kinds.size != ins.operands.size) { error("Operand count mismatch for ${ins.op}: expected ${kinds.size}, got ${ins.operands.size}") } for (i in kinds.indices) { - val v = ins.operands[i] + val operand = ins.operands[i] + val v = when (operand) { + is Operand.IntVal -> operand.value + is Operand.LabelRef -> labelIps[operand.label] + ?: error("Unknown label ${operand.label.id} for ${ins.op}") + } when (kinds[i]) { OperandKind.SLOT -> code.writeUInt(v, slotWidth) OperandKind.CONST -> code.writeUInt(v, constIdWidth) @@ -115,6 +151,16 @@ class BytecodeBuilder { } } + private fun operandWidth(kind: OperandKind, slotWidth: Int, constIdWidth: Int, ipWidth: Int): Int { + return when (kind) { + OperandKind.SLOT -> slotWidth + OperandKind.CONST -> constIdWidth + OperandKind.IP -> ipWidth + OperandKind.COUNT -> 2 + OperandKind.ID -> 2 + } + } + private enum class OperandKind { SLOT, CONST, 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 876e987..6f050da 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -17,12 +17,21 @@ package net.sergeych.lyng.bytecode import net.sergeych.lyng.ExpressionStatement +import net.sergeych.lyng.IfStatement import net.sergeych.lyng.obj.* class BytecodeCompiler { private val builder = BytecodeBuilder() private var nextSlot = 0 + fun compileStatement(name: String, stmt: net.sergeych.lyng.Statement): BytecodeFunction? { + return when (stmt) { + is ExpressionStatement -> compileExpression(name, stmt) + is net.sergeych.lyng.IfStatement -> compileIf(name, stmt) + else -> null + } + } + fun compileExpression(name: String, stmt: ExpressionStatement): BytecodeFunction? { val value = compileRef(stmt.ref) ?: return null builder.emit(Opcode.RET, value.slot) @@ -241,27 +250,61 @@ class BytecodeCompiler { return CompiledValue(slot, value.type) } + private fun compileIf(name: String, stmt: IfStatement): BytecodeFunction? { + val conditionStmt = stmt.condition as? ExpressionStatement ?: return null + val condValue = compileRef(conditionStmt.ref) ?: return null + if (condValue.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(condValue.slot), BytecodeBuilder.Operand.LabelRef(elseLabel)) + ) + val thenValue = compileStatementValue(stmt.ifBody) ?: return null + emitMove(thenValue, resultSlot) + builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel))) + + builder.mark(elseLabel) + if (stmt.elseBody != null) { + val elseValue = compileStatementValue(stmt.elseBody) ?: return null + emitMove(elseValue, resultSlot) + } else { + val id = builder.addConst(BytecodeConst.ObjRef(ObjVoid)) + builder.emit(Opcode.CONST_OBJ, id, resultSlot) + } + + builder.mark(endLabel) + builder.emit(Opcode.RET, resultSlot) + val localCount = maxOf(nextSlot, resultSlot + 1) + return builder.build(name, localCount) + } + + private fun compileStatementValue(stmt: net.sergeych.lyng.Statement): CompiledValue? { + return when (stmt) { + is ExpressionStatement -> compileRef(stmt.ref) + else -> null + } + } + + private fun emitMove(value: CompiledValue, dstSlot: Int) { + when (value.type) { + SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, dstSlot) + SlotType.REAL -> builder.emit(Opcode.MOVE_REAL, value.slot, dstSlot) + SlotType.BOOL -> builder.emit(Opcode.MOVE_BOOL, value.slot, dstSlot) + else -> builder.emit(Opcode.MOVE_OBJ, value.slot, dstSlot) + } + } + private fun refSlot(ref: LocalSlotRef): Int = ref.slot - - private fun refDepth(ref: LocalSlotRef): Int = refDepthAccessor(ref) - - private fun binaryLeft(ref: BinaryOpRef): ObjRef = binaryLeftAccessor(ref) - private fun binaryRight(ref: BinaryOpRef): ObjRef = binaryRightAccessor(ref) - private fun binaryOp(ref: BinaryOpRef): BinOp = binaryOpAccessor(ref) - - private fun unaryOperand(ref: UnaryOpRef): ObjRef = unaryOperandAccessor(ref) - private fun unaryOp(ref: UnaryOpRef): UnaryOp = unaryOpAccessor(ref) - - private fun assignTarget(ref: AssignRef): LocalSlotRef? = assignTargetAccessor(ref) - private fun assignValue(ref: AssignRef): ObjRef = assignValueAccessor(ref) - - // Accessor helpers to avoid exposing fields directly in ObjRef classes. - private fun refDepthAccessor(ref: LocalSlotRef): Int = ref.depth - private fun binaryLeftAccessor(ref: BinaryOpRef): ObjRef = ref.left - private fun binaryRightAccessor(ref: BinaryOpRef): ObjRef = ref.right - private fun binaryOpAccessor(ref: BinaryOpRef): BinOp = ref.op - private fun unaryOperandAccessor(ref: UnaryOpRef): ObjRef = ref.a - private fun unaryOpAccessor(ref: UnaryOpRef): UnaryOp = ref.op - private fun assignTargetAccessor(ref: AssignRef): LocalSlotRef? = ref.target as? LocalSlotRef - private fun assignValueAccessor(ref: AssignRef): ObjRef = ref.value + private fun refDepth(ref: LocalSlotRef): Int = ref.depth + private fun binaryLeft(ref: BinaryOpRef): ObjRef = ref.left + private fun binaryRight(ref: BinaryOpRef): ObjRef = ref.right + private fun binaryOp(ref: BinaryOpRef): BinOp = ref.op + private fun unaryOperand(ref: UnaryOpRef): ObjRef = ref.a + private fun unaryOp(ref: UnaryOpRef): UnaryOp = ref.op + private fun assignTarget(ref: AssignRef): LocalSlotRef? = ref.target as? LocalSlotRef + private fun assignValue(ref: AssignRef): ObjRef = ref.value } 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 4478b74..f085653 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeVm.kt @@ -109,6 +109,37 @@ class BytecodeVm { ip += fn.slotWidth frame.setInt(dst, frame.getInt(a) + frame.getInt(b)) } + Opcode.CMP_LT_INT -> { + val a = decoder.readSlot(code, ip) + ip += fn.slotWidth + val b = decoder.readSlot(code, ip) + ip += fn.slotWidth + val dst = decoder.readSlot(code, ip) + ip += fn.slotWidth + frame.setBool(dst, frame.getInt(a) < frame.getInt(b)) + } + Opcode.CMP_EQ_INT -> { + val a = decoder.readSlot(code, ip) + ip += fn.slotWidth + val b = decoder.readSlot(code, ip) + ip += fn.slotWidth + val dst = decoder.readSlot(code, ip) + ip += fn.slotWidth + frame.setBool(dst, frame.getInt(a) == frame.getInt(b)) + } + Opcode.JMP -> { + val target = decoder.readIp(code, ip, fn.ipWidth) + ip = target + } + Opcode.JMP_IF_FALSE -> { + val cond = decoder.readSlot(code, ip) + ip += fn.slotWidth + val target = decoder.readIp(code, ip, fn.ipWidth) + ip += fn.ipWidth + if (!frame.getBool(cond)) { + ip = target + } + } Opcode.RET -> { val slot = decoder.readSlot(code, ip) return slotToObj(frame, slot) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt index d22068b..0d43e5a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt @@ -20,6 +20,7 @@ package net.sergeych.lyng import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.ObjClass import net.sergeych.lyng.obj.ObjVoid +import net.sergeych.lyng.obj.toBool fun String.toSource(name: String = "eval"): Source = Source(name, this) @@ -63,6 +64,21 @@ abstract class Statement( } +class IfStatement( + val condition: Statement, + val ifBody: Statement, + val elseBody: Statement?, + override val pos: Pos, +) : Statement() { + override suspend fun execute(scope: Scope): Obj { + return if (condition.execute(scope).toBool()) { + ifBody.execute(scope) + } else { + elseBody?.execute(scope) ?: ObjVoid + } + } +} + class ExpressionStatement( val ref: net.sergeych.lyng.obj.ObjRef, override val pos: Pos diff --git a/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt b/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt index 511ee5a..18bb0bd 100644 --- a/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt +++ b/lynglib/src/commonTest/kotlin/BytecodeVmTest.kt @@ -14,11 +14,18 @@ * limitations under the License. */ +import net.sergeych.lyng.ExpressionStatement +import net.sergeych.lyng.IfStatement import net.sergeych.lyng.Scope import net.sergeych.lyng.bytecode.BytecodeBuilder +import net.sergeych.lyng.bytecode.BytecodeCompiler import net.sergeych.lyng.bytecode.BytecodeConst import net.sergeych.lyng.bytecode.BytecodeVm import net.sergeych.lyng.bytecode.Opcode +import net.sergeych.lyng.obj.BinaryOpRef +import net.sergeych.lyng.obj.BinOp +import net.sergeych.lyng.obj.ConstRef +import net.sergeych.lyng.obj.ObjInt import net.sergeych.lyng.obj.toInt import kotlin.test.Test import kotlin.test.assertEquals @@ -37,4 +44,28 @@ class BytecodeVmTest { val result = BytecodeVm().execute(fn, Scope(), emptyList()) assertEquals(5, result.toInt()) } + + @Test + fun ifExpressionReturnsThenValue() = kotlinx.coroutines.test.runTest { + val cond = ExpressionStatement( + BinaryOpRef( + BinOp.LT, + ConstRef(ObjInt.of(2).asReadonly), + ConstRef(ObjInt.of(3).asReadonly), + ), + net.sergeych.lyng.Pos.builtIn + ) + val thenStmt = ExpressionStatement( + ConstRef(ObjInt.of(10).asReadonly), + net.sergeych.lyng.Pos.builtIn + ) + val elseStmt = ExpressionStatement( + ConstRef(ObjInt.of(20).asReadonly), + net.sergeych.lyng.Pos.builtIn + ) + val ifStmt = IfStatement(cond, thenStmt, elseStmt, net.sergeych.lyng.Pos.builtIn) + val fn = BytecodeCompiler().compileStatement("ifTest", ifStmt) ?: error("bytecode compile failed") + val result = BytecodeVm().execute(fn, Scope(), emptyList()) + assertEquals(10, result.toInt()) + } }