Add bytecode if support and test

This commit is contained in:
Sergey Chernov 2026-01-25 19:05:42 +03:00
parent ea877748e5
commit 6560457e3d
6 changed files with 195 additions and 43 deletions

View File

@ -2965,25 +2965,10 @@ class Compiler(
return if (t2.type == Token.Type.ID && t2.value == "else") { return if (t2.type == Token.Type.ID && t2.value == "else") {
val elseBody = val elseBody =
parseStatement() ?: throw ScriptError(pos, "Bad else statement: expected statement") parseStatement() ?: throw ScriptError(pos, "Bad else statement: expected statement")
return object : Statement() { IfStatement(condition, ifBody, elseBody, start)
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)
}
}
} else { } else {
cc.previous() cc.previous()
object : Statement() { IfStatement(condition, ifBody, null, start)
override val pos: Pos = start
override suspend fun execute(scope: Scope): Obj {
if (condition.execute(scope).toBool())
return ifBody.execute(scope)
return ObjVoid
}
}
} }
} }

View File

@ -17,10 +17,19 @@
package net.sergeych.lyng.bytecode package net.sergeych.lyng.bytecode
class BytecodeBuilder { 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<Operand>)
private val instructions = mutableListOf<Instr>() private val instructions = mutableListOf<Instr>()
private val constPool = mutableListOf<BytecodeConst>() private val constPool = mutableListOf<BytecodeConst>()
private val labelPositions = mutableMapOf<Label, Int>()
private var nextLabelId = 0
fun addConst(c: BytecodeConst): Int { fun addConst(c: BytecodeConst): Int {
constPool += c constPool += c
@ -28,7 +37,17 @@ class BytecodeBuilder {
} }
fun emit(op: Opcode, vararg operands: Int) { 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<Operand>) {
instructions += Instr(op, operands)
}
fun label(): Label = Label(nextLabelId++)
fun mark(label: Label) {
labelPositions[label] = instructions.size
} }
fun build(name: String, localCount: Int): BytecodeFunction { fun build(name: String, localCount: Int): BytecodeFunction {
@ -39,15 +58,32 @@ class BytecodeBuilder {
} }
val constIdWidth = if (constPool.size < 65536) 2 else 4 val constIdWidth = if (constPool.size < 65536) 2 else 4
val ipWidth = 2 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<Label, Int>()
for ((label, idx) in labelPositions) {
labelIps[label] = instrOffsets.getOrNull(idx) ?: error("Invalid label index: $idx")
}
val code = ByteArrayOutput() val code = ByteArrayOutput()
for (ins in instructions) { for (ins in instructions) {
code.writeU8(ins.op.code.toInt() and 0xFF) code.writeU8(ins.op.code and 0xFF)
val kinds = operandKinds(ins.op) val kinds = operandKinds(ins.op)
if (kinds.size != ins.operands.size) { if (kinds.size != ins.operands.size) {
error("Operand count mismatch for ${ins.op}: expected ${kinds.size}, got ${ins.operands.size}") error("Operand count mismatch for ${ins.op}: expected ${kinds.size}, got ${ins.operands.size}")
} }
for (i in kinds.indices) { 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]) { when (kinds[i]) {
OperandKind.SLOT -> code.writeUInt(v, slotWidth) OperandKind.SLOT -> code.writeUInt(v, slotWidth)
OperandKind.CONST -> code.writeUInt(v, constIdWidth) 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 { private enum class OperandKind {
SLOT, SLOT,
CONST, CONST,

View File

@ -17,12 +17,21 @@
package net.sergeych.lyng.bytecode package net.sergeych.lyng.bytecode
import net.sergeych.lyng.ExpressionStatement import net.sergeych.lyng.ExpressionStatement
import net.sergeych.lyng.IfStatement
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
class BytecodeCompiler { class BytecodeCompiler {
private val builder = BytecodeBuilder() private val builder = BytecodeBuilder()
private var nextSlot = 0 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? { fun compileExpression(name: String, stmt: ExpressionStatement): BytecodeFunction? {
val value = compileRef(stmt.ref) ?: return null val value = compileRef(stmt.ref) ?: return null
builder.emit(Opcode.RET, value.slot) builder.emit(Opcode.RET, value.slot)
@ -241,27 +250,61 @@ class BytecodeCompiler {
return CompiledValue(slot, value.type) return CompiledValue(slot, value.type)
} }
private fun refSlot(ref: LocalSlotRef): Int = ref.slot 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
private fun refDepth(ref: LocalSlotRef): Int = refDepthAccessor(ref) val resultSlot = allocSlot()
val elseLabel = builder.label()
val endLabel = builder.label()
private fun binaryLeft(ref: BinaryOpRef): ObjRef = binaryLeftAccessor(ref) builder.emit(
private fun binaryRight(ref: BinaryOpRef): ObjRef = binaryRightAccessor(ref) Opcode.JMP_IF_FALSE,
private fun binaryOp(ref: BinaryOpRef): BinOp = binaryOpAccessor(ref) 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)))
private fun unaryOperand(ref: UnaryOpRef): ObjRef = unaryOperandAccessor(ref) builder.mark(elseLabel)
private fun unaryOp(ref: UnaryOpRef): UnaryOp = unaryOpAccessor(ref) if (stmt.elseBody != null) {
val elseValue = compileStatementValue(stmt.elseBody) ?: return null
private fun assignTarget(ref: AssignRef): LocalSlotRef? = assignTargetAccessor(ref) emitMove(elseValue, resultSlot)
private fun assignValue(ref: AssignRef): ObjRef = assignValueAccessor(ref) } else {
val id = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
// Accessor helpers to avoid exposing fields directly in ObjRef classes. builder.emit(Opcode.CONST_OBJ, id, resultSlot)
private fun refDepthAccessor(ref: LocalSlotRef): Int = ref.depth }
private fun binaryLeftAccessor(ref: BinaryOpRef): ObjRef = ref.left
private fun binaryRightAccessor(ref: BinaryOpRef): ObjRef = ref.right builder.mark(endLabel)
private fun binaryOpAccessor(ref: BinaryOpRef): BinOp = ref.op builder.emit(Opcode.RET, resultSlot)
private fun unaryOperandAccessor(ref: UnaryOpRef): ObjRef = ref.a val localCount = maxOf(nextSlot, resultSlot + 1)
private fun unaryOpAccessor(ref: UnaryOpRef): UnaryOp = ref.op return builder.build(name, localCount)
private fun assignTargetAccessor(ref: AssignRef): LocalSlotRef? = ref.target as? LocalSlotRef }
private fun assignValueAccessor(ref: AssignRef): ObjRef = ref.value
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 = 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
} }

View File

@ -109,6 +109,37 @@ class BytecodeVm {
ip += fn.slotWidth ip += fn.slotWidth
frame.setInt(dst, frame.getInt(a) + frame.getInt(b)) 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 -> { Opcode.RET -> {
val slot = decoder.readSlot(code, ip) val slot = decoder.readSlot(code, ip)
return slotToObj(frame, slot) return slotToObj(frame, slot)

View File

@ -20,6 +20,7 @@ package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjVoid import net.sergeych.lyng.obj.ObjVoid
import net.sergeych.lyng.obj.toBool
fun String.toSource(name: String = "eval"): Source = Source(name, this) 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( class ExpressionStatement(
val ref: net.sergeych.lyng.obj.ObjRef, val ref: net.sergeych.lyng.obj.ObjRef,
override val pos: Pos override val pos: Pos

View File

@ -14,11 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
import net.sergeych.lyng.ExpressionStatement
import net.sergeych.lyng.IfStatement
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.bytecode.BytecodeBuilder import net.sergeych.lyng.bytecode.BytecodeBuilder
import net.sergeych.lyng.bytecode.BytecodeCompiler
import net.sergeych.lyng.bytecode.BytecodeConst import net.sergeych.lyng.bytecode.BytecodeConst
import net.sergeych.lyng.bytecode.BytecodeVm import net.sergeych.lyng.bytecode.BytecodeVm
import net.sergeych.lyng.bytecode.Opcode 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 net.sergeych.lyng.obj.toInt
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -37,4 +44,28 @@ class BytecodeVmTest {
val result = BytecodeVm().execute(fn, Scope(), emptyList()) val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(5, result.toInt()) 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())
}
} }