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") {
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)
}
}

View File

@ -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<Operand>)
private val instructions = mutableListOf<Instr>()
private val constPool = mutableListOf<BytecodeConst>()
private val labelPositions = mutableMapOf<Label, Int>()
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<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 {
@ -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<Label, Int>()
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,

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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())
}
}