Add minimal bytecode VM execution test

This commit is contained in:
Sergey Chernov 2026-01-25 17:17:14 +03:00
parent d8b00a805c
commit ea877748e5
5 changed files with 131 additions and 8 deletions

View File

@ -25,7 +25,7 @@ interface BytecodeDecoder {
object Decoder8 : BytecodeDecoder { object Decoder8 : BytecodeDecoder {
override fun readOpcode(code: ByteArray, ip: Int): Opcode = override fun readOpcode(code: ByteArray, ip: Int): Opcode =
Opcode.fromCode(code[ip]) ?: error("Unknown opcode: ${code[ip]}") Opcode.fromCode(code[ip].toInt() and 0xFF) ?: error("Unknown opcode: ${code[ip]}")
override fun readSlot(code: ByteArray, ip: Int): Int = code[ip].toInt() and 0xFF override fun readSlot(code: ByteArray, ip: Int): Int = code[ip].toInt() and 0xFF
@ -38,7 +38,7 @@ object Decoder8 : BytecodeDecoder {
object Decoder16 : BytecodeDecoder { object Decoder16 : BytecodeDecoder {
override fun readOpcode(code: ByteArray, ip: Int): Opcode = override fun readOpcode(code: ByteArray, ip: Int): Opcode =
Opcode.fromCode(code[ip]) ?: error("Unknown opcode: ${code[ip]}") Opcode.fromCode(code[ip].toInt() and 0xFF) ?: error("Unknown opcode: ${code[ip]}")
override fun readSlot(code: ByteArray, ip: Int): Int = override fun readSlot(code: ByteArray, ip: Int): Int =
(code[ip].toInt() and 0xFF) or ((code[ip + 1].toInt() and 0xFF) shl 8) (code[ip].toInt() and 0xFF) or ((code[ip + 1].toInt() and 0xFF) shl 8)
@ -52,7 +52,7 @@ object Decoder16 : BytecodeDecoder {
object Decoder32 : BytecodeDecoder { object Decoder32 : BytecodeDecoder {
override fun readOpcode(code: ByteArray, ip: Int): Opcode = override fun readOpcode(code: ByteArray, ip: Int): Opcode =
Opcode.fromCode(code[ip]) ?: error("Unknown opcode: ${code[ip]}") Opcode.fromCode(code[ip].toInt() and 0xFF) ?: error("Unknown opcode: ${code[ip]}")
override fun readSlot(code: ByteArray, ip: Int): Int = readUInt(code, ip, 4) override fun readSlot(code: ByteArray, ip: Int): Int = readUInt(code, ip, 4)

View File

@ -33,6 +33,7 @@ class BytecodeFrame(
private val boolSlots: BooleanArray = BooleanArray(slotCount) private val boolSlots: BooleanArray = BooleanArray(slotCount)
fun getSlotType(slot: Int): SlotType = SlotType.values().first { it.code == slotTypes[slot] } fun getSlotType(slot: Int): SlotType = SlotType.values().first { it.code == slotTypes[slot] }
fun getSlotTypeCode(slot: Int): Byte = slotTypes[slot]
fun setSlotType(slot: Int, type: SlotType) { fun setSlotType(slot: Int, type: SlotType) {
slotTypes[slot] = type.code slotTypes[slot] = type.code
} }

View File

@ -17,8 +17,7 @@
package net.sergeych.lyng.bytecode package net.sergeych.lyng.bytecode
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.*
import net.sergeych.lyng.obj.ObjVoid
class BytecodeVm { class BytecodeVm {
suspend fun execute(fn: BytecodeFunction, scope: Scope, args: List<Obj>): Obj { suspend fun execute(fn: BytecodeFunction, scope: Scope, args: List<Obj>): Obj {
@ -41,10 +40,93 @@ class BytecodeVm {
Opcode.NOP -> { Opcode.NOP -> {
// no-op // no-op
} }
Opcode.CONST_INT -> {
val constId = decoder.readConstId(code, ip, fn.constIdWidth)
ip += fn.constIdWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
val c = fn.constants[constId] as? BytecodeConst.IntVal
?: error("CONST_INT expects IntVal at $constId")
frame.setInt(dst, c.value)
}
Opcode.CONST_REAL -> {
val constId = decoder.readConstId(code, ip, fn.constIdWidth)
ip += fn.constIdWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
val c = fn.constants[constId] as? BytecodeConst.RealVal
?: error("CONST_REAL expects RealVal at $constId")
frame.setReal(dst, c.value)
}
Opcode.CONST_BOOL -> {
val constId = decoder.readConstId(code, ip, fn.constIdWidth)
ip += fn.constIdWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
val c = fn.constants[constId] as? BytecodeConst.Bool
?: error("CONST_BOOL expects Bool at $constId")
frame.setBool(dst, c.value)
}
Opcode.CONST_NULL -> {
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
frame.setObj(dst, ObjNull)
}
Opcode.MOVE_INT -> {
val src = decoder.readSlot(code, ip)
ip += fn.slotWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
frame.setInt(dst, frame.getInt(src))
}
Opcode.MOVE_REAL -> {
val src = decoder.readSlot(code, ip)
ip += fn.slotWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
frame.setReal(dst, frame.getReal(src))
}
Opcode.MOVE_BOOL -> {
val src = decoder.readSlot(code, ip)
ip += fn.slotWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
frame.setBool(dst, frame.getBool(src))
}
Opcode.MOVE_OBJ -> {
val src = decoder.readSlot(code, ip)
ip += fn.slotWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
frame.setObj(dst, frame.getObj(src))
}
Opcode.ADD_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.setInt(dst, frame.getInt(a) + frame.getInt(b))
}
Opcode.RET -> {
val slot = decoder.readSlot(code, ip)
return slotToObj(frame, slot)
}
Opcode.RET_VOID -> return ObjVoid Opcode.RET_VOID -> return ObjVoid
else -> error("Opcode not implemented: $op") else -> error("Opcode not implemented: $op")
} }
} }
return ObjVoid return ObjVoid
} }
private fun slotToObj(frame: BytecodeFrame, slot: Int): Obj {
return when (frame.getSlotTypeCode(slot)) {
SlotType.INT.code -> ObjInt.of(frame.getInt(slot))
SlotType.REAL.code -> ObjReal.of(frame.getReal(slot))
SlotType.BOOL.code -> if (frame.getBool(slot)) ObjTrue else ObjFalse
SlotType.OBJ.code -> frame.getObj(slot)
else -> ObjVoid
}
}
} }

View File

@ -16,7 +16,7 @@
package net.sergeych.lyng.bytecode package net.sergeych.lyng.bytecode
enum class Opcode(val code: Byte) { enum class Opcode(val code: Int) {
NOP(0x00), NOP(0x00),
MOVE_OBJ(0x01), MOVE_OBJ(0x01),
MOVE_INT(0x02), MOVE_INT(0x02),
@ -105,7 +105,7 @@ enum class Opcode(val code: Byte) {
; ;
companion object { companion object {
private val byCode: Map<Byte, Opcode> = values().associateBy { it.code } private val byCode: Map<Int, Opcode> = values().associateBy { it.code }
fun fromCode(code: Byte): Opcode? = byCode[code] fun fromCode(code: Int): Opcode? = byCode[code]
} }
} }

View File

@ -0,0 +1,40 @@
/*
* 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.
*/
import net.sergeych.lyng.Scope
import net.sergeych.lyng.bytecode.BytecodeBuilder
import net.sergeych.lyng.bytecode.BytecodeConst
import net.sergeych.lyng.bytecode.BytecodeVm
import net.sergeych.lyng.bytecode.Opcode
import net.sergeych.lyng.obj.toInt
import kotlin.test.Test
import kotlin.test.assertEquals
class BytecodeVmTest {
@Test
fun addsIntConstants() = kotlinx.coroutines.test.runTest {
val builder = BytecodeBuilder()
val k0 = builder.addConst(BytecodeConst.IntVal(2))
val k1 = builder.addConst(BytecodeConst.IntVal(3))
builder.emit(Opcode.CONST_INT, k0, 0)
builder.emit(Opcode.CONST_INT, k1, 1)
builder.emit(Opcode.ADD_INT, 0, 1, 2)
builder.emit(Opcode.RET, 2)
val fn = builder.build("addInts", localCount = 3)
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(5, result.toInt())
}
}