diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt new file mode 100644 index 0000000..6fa7486 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeBuilder.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sergeych.lyng.bytecode + +class BytecodeBuilder { + data class Instr(val op: Opcode, val operands: IntArray) + + private val instructions = mutableListOf() + private val constPool = mutableListOf() + + fun addConst(c: BytecodeConst): Int { + constPool += c + return constPool.lastIndex + } + + fun emit(op: Opcode, vararg operands: Int) { + instructions += Instr(op, operands.copyOf()) + } + + fun build(name: String, localCount: Int): BytecodeFunction { + val slotWidth = when { + localCount < 256 -> 1 + localCount < 65536 -> 2 + else -> 4 + } + val constIdWidth = if (constPool.size < 65536) 2 else 4 + val ipWidth = 2 + val code = ByteArrayOutput() + for (ins in instructions) { + code.writeU8(ins.op.code.toInt() 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] + when (kinds[i]) { + OperandKind.SLOT -> code.writeUInt(v, slotWidth) + OperandKind.CONST -> code.writeUInt(v, constIdWidth) + OperandKind.IP -> code.writeUInt(v, ipWidth) + OperandKind.COUNT -> code.writeUInt(v, 2) + OperandKind.ID -> code.writeUInt(v, 2) + } + } + } + return BytecodeFunction( + name = name, + localCount = localCount, + slotWidth = slotWidth, + ipWidth = ipWidth, + constIdWidth = constIdWidth, + constants = constPool.toList(), + code = code.toByteArray() + ) + } + + private fun operandKinds(op: Opcode): List { + return when (op) { + Opcode.NOP, Opcode.RET_VOID -> emptyList() + Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, + 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 -> + listOf(OperandKind.SLOT, OperandKind.SLOT) + Opcode.CONST_NULL -> + listOf(OperandKind.SLOT) + Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL -> + listOf(OperandKind.CONST, OperandKind.SLOT) + 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, + Opcode.CMP_EQ_INT, Opcode.CMP_NEQ_INT, Opcode.CMP_LT_INT, Opcode.CMP_LTE_INT, + Opcode.CMP_GT_INT, Opcode.CMP_GTE_INT, + Opcode.CMP_EQ_REAL, Opcode.CMP_NEQ_REAL, Opcode.CMP_LT_REAL, Opcode.CMP_LTE_REAL, + Opcode.CMP_GT_REAL, Opcode.CMP_GTE_REAL, + Opcode.CMP_EQ_BOOL, Opcode.CMP_NEQ_BOOL, + Opcode.CMP_EQ_INT_REAL, Opcode.CMP_EQ_REAL_INT, Opcode.CMP_LT_INT_REAL, Opcode.CMP_LT_REAL_INT, + Opcode.CMP_LTE_INT_REAL, Opcode.CMP_LTE_REAL_INT, Opcode.CMP_GT_INT_REAL, Opcode.CMP_GT_REAL_INT, + Opcode.CMP_GTE_INT_REAL, Opcode.CMP_GTE_REAL_INT, + Opcode.AND_BOOL, Opcode.OR_BOOL -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET -> + listOf(OperandKind.SLOT) + Opcode.JMP -> + listOf(OperandKind.IP) + Opcode.JMP_IF_TRUE, Opcode.JMP_IF_FALSE -> + listOf(OperandKind.SLOT, OperandKind.IP) + Opcode.CALL_DIRECT, Opcode.CALL_FALLBACK -> + listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) + Opcode.CALL_VIRTUAL -> + listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT) + Opcode.GET_FIELD -> + listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT) + Opcode.SET_FIELD -> + listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT) + Opcode.GET_INDEX -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.SET_INDEX -> + listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) + Opcode.EVAL_FALLBACK -> + listOf(OperandKind.ID, OperandKind.SLOT) + } + } + + private enum class OperandKind { + SLOT, + CONST, + IP, + COUNT, + ID, + } + + private class ByteArrayOutput { + private val data = ArrayList(256) + + fun writeU8(v: Int) { + data.add((v and 0xFF).toByte()) + } + + fun writeUInt(v: Int, width: Int) { + var value = v + var remaining = width + while (remaining-- > 0) { + writeU8(value) + value = value ushr 8 + } + } + + fun toByteArray(): ByteArray = data.toByteArray() + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt new file mode 100644 index 0000000..876e987 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -0,0 +1,267 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sergeych.lyng.bytecode + +import net.sergeych.lyng.ExpressionStatement +import net.sergeych.lyng.obj.* + +class BytecodeCompiler { + private val builder = BytecodeBuilder() + private var nextSlot = 0 + + fun compileExpression(name: String, stmt: ExpressionStatement): BytecodeFunction? { + val value = compileRef(stmt.ref) ?: return null + builder.emit(Opcode.RET, value.slot) + val localCount = maxOf(nextSlot, value.slot + 1) + return builder.build(name, localCount) + } + + private data class CompiledValue(val slot: Int, val type: SlotType) + + private fun allocSlot(): Int = nextSlot++ + + private fun compileRef(ref: ObjRef): CompiledValue? { + return when (ref) { + is ConstRef -> compileConst(ref.constValue) + is LocalSlotRef -> { + if (ref.name.isEmpty()) return null + if (refDepth(ref) != 0) return null + CompiledValue(refSlot(ref), SlotType.UNKNOWN) + } + is BinaryOpRef -> compileBinary(ref) + is UnaryOpRef -> compileUnary(ref) + is AssignRef -> compileAssign(ref) + else -> null + } + } + + private fun compileConst(obj: Obj): CompiledValue? { + val slot = allocSlot() + when (obj) { + is ObjInt -> { + val id = builder.addConst(BytecodeConst.IntVal(obj.value)) + builder.emit(Opcode.CONST_INT, id, slot) + return CompiledValue(slot, SlotType.INT) + } + is ObjReal -> { + val id = builder.addConst(BytecodeConst.RealVal(obj.value)) + builder.emit(Opcode.CONST_REAL, id, slot) + return CompiledValue(slot, SlotType.REAL) + } + is ObjBool -> { + val id = builder.addConst(BytecodeConst.Bool(obj.value)) + builder.emit(Opcode.CONST_BOOL, id, slot) + return CompiledValue(slot, SlotType.BOOL) + } + is ObjString -> { + val id = builder.addConst(BytecodeConst.StringVal(obj.value)) + builder.emit(Opcode.CONST_OBJ, id, slot) + return CompiledValue(slot, SlotType.OBJ) + } + ObjNull -> { + builder.emit(Opcode.CONST_NULL, slot) + return CompiledValue(slot, SlotType.OBJ) + } + else -> { + val id = builder.addConst(BytecodeConst.ObjRef(obj)) + builder.emit(Opcode.CONST_OBJ, id, slot) + return CompiledValue(slot, SlotType.OBJ) + } + } + } + + private fun compileUnary(ref: UnaryOpRef): CompiledValue? { + val a = compileRef(unaryOperand(ref)) ?: return null + val out = allocSlot() + return when (unaryOp(ref)) { + UnaryOp.NEGATE -> when (a.type) { + SlotType.INT -> { + builder.emit(Opcode.NEG_INT, a.slot, out) + CompiledValue(out, SlotType.INT) + } + SlotType.REAL -> { + builder.emit(Opcode.NEG_REAL, a.slot, out) + CompiledValue(out, SlotType.REAL) + } + else -> null + } + UnaryOp.NOT -> { + if (a.type != SlotType.BOOL) return null + builder.emit(Opcode.NOT_BOOL, a.slot, out) + CompiledValue(out, SlotType.BOOL) + } + UnaryOp.BITNOT -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.INV_INT, a.slot, out) + CompiledValue(out, SlotType.INT) + } + } + } + + private fun compileBinary(ref: BinaryOpRef): CompiledValue? { + val a = compileRef(binaryLeft(ref)) ?: return null + val b = compileRef(binaryRight(ref)) ?: return null + if (a.type != b.type && a.type != SlotType.UNKNOWN && b.type != SlotType.UNKNOWN) return null + val out = allocSlot() + val op = binaryOp(ref) + return when (op) { + BinOp.PLUS -> when (a.type) { + SlotType.INT -> { + builder.emit(Opcode.ADD_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.INT) + } + SlotType.REAL -> { + builder.emit(Opcode.ADD_REAL, a.slot, b.slot, out) + CompiledValue(out, SlotType.REAL) + } + else -> null + } + BinOp.MINUS -> when (a.type) { + SlotType.INT -> { + builder.emit(Opcode.SUB_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.INT) + } + SlotType.REAL -> { + builder.emit(Opcode.SUB_REAL, a.slot, b.slot, out) + CompiledValue(out, SlotType.REAL) + } + else -> null + } + BinOp.STAR -> when (a.type) { + SlotType.INT -> { + builder.emit(Opcode.MUL_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.INT) + } + SlotType.REAL -> { + builder.emit(Opcode.MUL_REAL, a.slot, b.slot, out) + CompiledValue(out, SlotType.REAL) + } + else -> null + } + BinOp.SLASH -> when (a.type) { + SlotType.INT -> { + builder.emit(Opcode.DIV_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.INT) + } + SlotType.REAL -> { + builder.emit(Opcode.DIV_REAL, a.slot, b.slot, out) + CompiledValue(out, SlotType.REAL) + } + else -> null + } + BinOp.PERCENT -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.MOD_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.INT) + } + BinOp.EQ -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.CMP_EQ_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.BOOL) + } + BinOp.NEQ -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.CMP_NEQ_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.BOOL) + } + BinOp.LT -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.CMP_LT_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.BOOL) + } + BinOp.LTE -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.CMP_LTE_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.BOOL) + } + BinOp.GT -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.CMP_GT_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.BOOL) + } + BinOp.GTE -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.CMP_GTE_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.BOOL) + } + BinOp.BAND -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.AND_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.INT) + } + BinOp.BOR -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.OR_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.INT) + } + BinOp.BXOR -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.XOR_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.INT) + } + BinOp.SHL -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.SHL_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.INT) + } + BinOp.SHR -> { + if (a.type != SlotType.INT) return null + builder.emit(Opcode.SHR_INT, a.slot, b.slot, out) + CompiledValue(out, SlotType.INT) + } + else -> null + } + } + + private fun compileAssign(ref: AssignRef): CompiledValue? { + val target = assignTarget(ref) ?: return null + if (refDepth(target) != 0) return null + val value = compileRef(assignValue(ref)) ?: return null + val slot = refSlot(target) + when (value.type) { + SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, slot) + SlotType.REAL -> builder.emit(Opcode.MOVE_REAL, value.slot, slot) + SlotType.BOOL -> builder.emit(Opcode.MOVE_BOOL, value.slot, slot) + else -> builder.emit(Opcode.MOVE_OBJ, value.slot, slot) + } + return CompiledValue(slot, value.type) + } + + 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 +} 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 11df61e..8463ce4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -89,7 +89,7 @@ enum class BinOp { } /** R-value reference for unary operations. */ -class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef { +class UnaryOpRef(internal val op: UnaryOp, internal val a: ObjRef) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { val v = a.evalValue(scope) if (PerfFlags.PRIMITIVE_FASTOPS) { @@ -141,7 +141,7 @@ class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef { } /** R-value reference for binary operations. */ -class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef { +class BinaryOpRef(internal val op: BinOp, internal val left: ObjRef, internal val right: ObjRef) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { return evalValue(scope).asReadonly } @@ -2403,8 +2403,8 @@ class ImplicitThisMethodCallRef( */ class LocalSlotRef( val name: String, - private val slot: Int, - private val depth: Int, + internal val slot: Int, + internal val depth: Int, private val atPos: Pos, ) : ObjRef { override fun forEachVariable(block: (String) -> Unit) { @@ -2657,8 +2657,8 @@ class AssignIfNullRef( /** Simple assignment: target = value */ class AssignRef( - private val target: ObjRef, - private val value: ObjRef, + internal val target: ObjRef, + internal val value: ObjRef, private val atPos: Pos, ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord {