Compare commits

...

10 Commits

15 changed files with 2402 additions and 27 deletions

View File

@ -149,6 +149,16 @@ Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass.
- CMP_GT_REAL_INT S, S -> S
- CMP_GTE_INT_REAL S, S -> S
- CMP_GTE_REAL_INT S, S -> S
- CMP_NEQ_INT_REAL S, S -> S
- CMP_NEQ_REAL_INT S, S -> S
- CMP_EQ_OBJ S, S -> S
- CMP_NEQ_OBJ S, S -> S
- CMP_REF_EQ_OBJ S, S -> S
- CMP_REF_NEQ_OBJ S, S -> S
- CMP_LT_OBJ S, S -> S
- CMP_LTE_OBJ S, S -> S
- CMP_GT_OBJ S, S -> S
- CMP_GTE_OBJ S, S -> S
### Boolean ops
- NOT_BOOL S -> S
@ -176,7 +186,24 @@ Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass.
### Fallback
- EVAL_FALLBACK T -> S
## 6) Function Header (binary container)
## 6) Const Pool Encoding (v0)
Each const entry is encoded as:
[tag:U8] [payload...]
Tags:
- 0x00: NULL
- 0x01: BOOL (payload: U8 0/1)
- 0x02: INT (payload: S64, little-endian)
- 0x03: REAL (payload: F64, IEEE-754, little-endian)
- 0x04: STRING (payload: U32 length + UTF-8 bytes)
- 0x05: OBJ_REF (payload: U32 index into external Obj table)
Notes:
- OBJ_REF is reserved for embedding prebuilt Obj handles if needed.
- Strings use UTF-8; length is bytes, not chars.
## 7) Function Header (binary container)
Suggested layout for a bytecode function blob:
- magic: U32 ("LYBC")
@ -190,10 +217,34 @@ Suggested layout for a bytecode function blob:
- constPool: [const entries...]
- code: [bytecode...]
Const pool entries are encoded as type-tagged values (Obj/Int/Real/Bool/String)
in a simple tagged format. This is intentionally unspecified in v0.
Const pool entries use the encoding described in section 6.
## 7) Notes
## 8) Sample Bytecode (illustrative)
Example Lyng:
val x = 2
val y = 3
val z = x + y
Assume:
- localCount = 3 (x,y,z)
- argCount = 0
- slot width = 1 byte
- const pool: [INT 2, INT 3]
Bytecode:
CONST_INT k0 -> s0
CONST_INT k1 -> s1
ADD_INT s0, s1 -> s2
RET_VOID
Encoded (opcode values symbolic):
[OP_CONST_INT][k0][s0]
[OP_CONST_INT][k1][s1]
[OP_ADD_INT][s0][s1][s2]
[OP_RET_VOID]
## 9) Notes
- Mixed-mode is allowed: compiler can emit FALLBACK ops for unsupported nodes.
- The VM must be suspendable; on suspension, store ip + minimal operand state.

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

@ -0,0 +1,199 @@
/*
* 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 {
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
private val fallbackStatements = mutableListOf<net.sergeych.lyng.Statement>()
fun addConst(c: BytecodeConst): Int {
constPool += c
return constPool.lastIndex
}
fun emit(op: Opcode, vararg operands: Int) {
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 addFallback(stmt: net.sergeych.lyng.Statement): Int {
fallbackStatements += stmt
return fallbackStatements.lastIndex
}
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 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 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 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)
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(),
fallbackStatements = fallbackStatements.toList(),
code = code.toByteArray()
)
}
private fun operandKinds(op: Opcode): List<OperandKind> {
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.CMP_NEQ_INT_REAL, Opcode.CMP_NEQ_REAL_INT,
Opcode.CMP_EQ_OBJ, Opcode.CMP_NEQ_OBJ, Opcode.CMP_REF_EQ_OBJ, Opcode.CMP_REF_NEQ_OBJ,
Opcode.CMP_LT_OBJ, Opcode.CMP_LTE_OBJ, Opcode.CMP_GT_OBJ, Opcode.CMP_GTE_OBJ,
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 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,
IP,
COUNT,
ID,
}
private class ByteArrayOutput {
private val data = ArrayList<Byte>(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()
}
}

View File

@ -0,0 +1,623 @@
/*
* 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.IfStatement
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Statement
import net.sergeych.lyng.ToBoolStatement
import net.sergeych.lyng.obj.*
class BytecodeCompiler {
private val builder = BytecodeBuilder()
private var nextSlot = 0
private val slotTypes = mutableMapOf<Int, SlotType>()
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 = compileRefWithFallback(stmt.ref, null, stmt.pos) ?: 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), slotTypes[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 op = binaryOp(ref)
if (op == BinOp.AND || op == BinOp.OR) {
return compileLogical(op, binaryLeft(ref), binaryRight(ref), refPos(ref))
}
val a = compileRef(binaryLeft(ref)) ?: return null
val b = compileRef(binaryRight(ref)) ?: return null
val typesMismatch = a.type != b.type && a.type != SlotType.UNKNOWN && b.type != SlotType.UNKNOWN
if (typesMismatch && op !in setOf(BinOp.EQ, BinOp.NEQ, BinOp.LT, BinOp.LTE, BinOp.GT, BinOp.GTE)) {
return null
}
val out = allocSlot()
return when (op) {
BinOp.PLUS -> when (a.type) {
SlotType.INT -> {
when (b.type) {
SlotType.INT -> {
builder.emit(Opcode.ADD_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.REAL -> compileRealArithmeticWithCoercion(Opcode.ADD_REAL, a, b, out)
else -> null
}
}
SlotType.REAL -> {
when (b.type) {
SlotType.REAL -> {
builder.emit(Opcode.ADD_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.REAL)
}
SlotType.INT -> compileRealArithmeticWithCoercion(Opcode.ADD_REAL, a, b, out)
else -> null
}
}
else -> null
}
BinOp.MINUS -> when (a.type) {
SlotType.INT -> {
when (b.type) {
SlotType.INT -> {
builder.emit(Opcode.SUB_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.REAL -> compileRealArithmeticWithCoercion(Opcode.SUB_REAL, a, b, out)
else -> null
}
}
SlotType.REAL -> {
when (b.type) {
SlotType.REAL -> {
builder.emit(Opcode.SUB_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.REAL)
}
SlotType.INT -> compileRealArithmeticWithCoercion(Opcode.SUB_REAL, a, b, out)
else -> null
}
}
else -> null
}
BinOp.STAR -> when (a.type) {
SlotType.INT -> {
when (b.type) {
SlotType.INT -> {
builder.emit(Opcode.MUL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.REAL -> compileRealArithmeticWithCoercion(Opcode.MUL_REAL, a, b, out)
else -> null
}
}
SlotType.REAL -> {
when (b.type) {
SlotType.REAL -> {
builder.emit(Opcode.MUL_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.REAL)
}
SlotType.INT -> compileRealArithmeticWithCoercion(Opcode.MUL_REAL, a, b, out)
else -> null
}
}
else -> null
}
BinOp.SLASH -> when (a.type) {
SlotType.INT -> {
when (b.type) {
SlotType.INT -> {
builder.emit(Opcode.DIV_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.REAL -> compileRealArithmeticWithCoercion(Opcode.DIV_REAL, a, b, out)
else -> null
}
}
SlotType.REAL -> {
when (b.type) {
SlotType.REAL -> {
builder.emit(Opcode.DIV_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.REAL)
}
SlotType.INT -> compileRealArithmeticWithCoercion(Opcode.DIV_REAL, a, b, out)
else -> null
}
}
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 -> {
compileCompareEq(a, b, out)
}
BinOp.NEQ -> {
compileCompareNeq(a, b, out)
}
BinOp.LT -> {
compileCompareLt(a, b, out)
}
BinOp.LTE -> {
compileCompareLte(a, b, out)
}
BinOp.GT -> {
compileCompareGt(a, b, out)
}
BinOp.GTE -> {
compileCompareGte(a, b, out)
}
BinOp.REF_EQ -> {
if (a.type != SlotType.OBJ || b.type != SlotType.OBJ) return null
builder.emit(Opcode.CMP_REF_EQ_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
BinOp.REF_NEQ -> {
if (a.type != SlotType.OBJ || b.type != SlotType.OBJ) return null
builder.emit(Opcode.CMP_REF_NEQ_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
BinOp.AND -> {
if (a.type != SlotType.BOOL) return null
builder.emit(Opcode.AND_BOOL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
BinOp.OR -> {
if (a.type != SlotType.BOOL) return null
builder.emit(Opcode.OR_BOOL, 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 compileRealArithmeticWithCoercion(
op: Opcode,
a: CompiledValue,
b: CompiledValue,
out: Int
): CompiledValue? {
if (a.type == SlotType.INT && b.type == SlotType.REAL) {
val left = allocSlot()
builder.emit(Opcode.INT_TO_REAL, a.slot, left)
builder.emit(op, left, b.slot, out)
return CompiledValue(out, SlotType.REAL)
}
if (a.type == SlotType.REAL && b.type == SlotType.INT) {
val right = allocSlot()
builder.emit(Opcode.INT_TO_REAL, b.slot, right)
builder.emit(op, a.slot, right, out)
return CompiledValue(out, SlotType.REAL)
}
return null
}
private fun compileCompareEq(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
return when {
a.type == SlotType.INT && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_EQ_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_EQ_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.BOOL && b.type == SlotType.BOOL -> {
builder.emit(Opcode.CMP_EQ_BOOL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.INT && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_EQ_INT_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_EQ_REAL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.OBJ && b.type == SlotType.OBJ -> {
builder.emit(Opcode.CMP_EQ_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
else -> null
}
}
private fun compileCompareNeq(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
return when {
a.type == SlotType.INT && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_NEQ_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_NEQ_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.BOOL && b.type == SlotType.BOOL -> {
builder.emit(Opcode.CMP_NEQ_BOOL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.INT && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_NEQ_INT_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_NEQ_REAL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.OBJ && b.type == SlotType.OBJ -> {
builder.emit(Opcode.CMP_NEQ_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
else -> null
}
}
private fun compileCompareLt(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
return when {
a.type == SlotType.INT && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_LT_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_LT_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.INT && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_LT_INT_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_LT_REAL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.OBJ && b.type == SlotType.OBJ -> {
builder.emit(Opcode.CMP_LT_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
else -> null
}
}
private fun compileCompareLte(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
return when {
a.type == SlotType.INT && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_LTE_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_LTE_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.INT && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_LTE_INT_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_LTE_REAL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.OBJ && b.type == SlotType.OBJ -> {
builder.emit(Opcode.CMP_LTE_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
else -> null
}
}
private fun compileCompareGt(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
return when {
a.type == SlotType.INT && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_GT_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_GT_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.INT && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_GT_INT_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_GT_REAL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.OBJ && b.type == SlotType.OBJ -> {
builder.emit(Opcode.CMP_GT_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
else -> null
}
}
private fun compileCompareGte(a: CompiledValue, b: CompiledValue, out: Int): CompiledValue? {
if (a.type == SlotType.UNKNOWN || b.type == SlotType.UNKNOWN) return null
return when {
a.type == SlotType.INT && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_GTE_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_GTE_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.INT && b.type == SlotType.REAL -> {
builder.emit(Opcode.CMP_GTE_INT_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.REAL && b.type == SlotType.INT -> {
builder.emit(Opcode.CMP_GTE_REAL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
a.type == SlotType.OBJ && b.type == SlotType.OBJ -> {
builder.emit(Opcode.CMP_GTE_OBJ, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
else -> null
}
}
private fun compileLogical(op: BinOp, left: ObjRef, right: ObjRef, pos: Pos): CompiledValue? {
val leftValue = compileRefWithFallback(left, SlotType.BOOL, pos) ?: return null
if (leftValue.type != SlotType.BOOL) return null
val resultSlot = allocSlot()
val shortLabel = builder.label()
val endLabel = builder.label()
if (op == BinOp.AND) {
builder.emit(
Opcode.JMP_IF_FALSE,
listOf(BytecodeBuilder.Operand.IntVal(leftValue.slot), BytecodeBuilder.Operand.LabelRef(shortLabel))
)
} else {
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(BytecodeBuilder.Operand.IntVal(leftValue.slot), BytecodeBuilder.Operand.LabelRef(shortLabel))
)
}
val rightValue = compileRefWithFallback(right, SlotType.BOOL, pos) ?: return null
emitMove(rightValue, resultSlot)
builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel)))
builder.mark(shortLabel)
val constId = builder.addConst(BytecodeConst.Bool(op == BinOp.OR))
builder.emit(Opcode.CONST_BOOL, constId, resultSlot)
builder.mark(endLabel)
return CompiledValue(resultSlot, SlotType.BOOL)
}
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)
}
updateSlotType(slot, value.type)
return CompiledValue(slot, value.type)
}
private fun compileIf(name: String, stmt: IfStatement): BytecodeFunction? {
val conditionStmt = stmt.condition as? ExpressionStatement ?: return null
val condValue = compileRefWithFallback(conditionStmt.ref, SlotType.BOOL, stmt.pos) ?: 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: Statement): CompiledValue? {
return when (stmt) {
is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos)
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 compileRefWithFallback(ref: ObjRef, forceType: SlotType?, pos: Pos): CompiledValue? {
val compiled = compileRef(ref)
if (compiled != null && (forceType == null || compiled.type == forceType || compiled.type == SlotType.UNKNOWN)) {
return if (forceType != null && compiled.type == SlotType.UNKNOWN) {
CompiledValue(compiled.slot, forceType)
} else compiled
}
val slot = allocSlot()
val stmt = if (forceType == SlotType.BOOL) {
ToBoolStatement(ExpressionStatement(ref, pos), pos)
} else {
ExpressionStatement(ref, pos)
}
val id = builder.addFallback(stmt)
builder.emit(Opcode.EVAL_FALLBACK, id, slot)
updateSlotType(slot, forceType ?: SlotType.OBJ)
return CompiledValue(slot, forceType ?: SlotType.OBJ)
}
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
private fun refPos(ref: BinaryOpRef): Pos = Pos.builtIn
private fun updateSlotType(slot: Int, type: SlotType) {
if (type == SlotType.UNKNOWN) {
slotTypes.remove(slot)
} else {
slotTypes[slot] = type
}
}
}

View File

@ -0,0 +1,28 @@
/*
* 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.obj.Obj
sealed class BytecodeConst {
object Null : BytecodeConst()
data class Bool(val value: Boolean) : BytecodeConst()
data class IntVal(val value: Long) : BytecodeConst()
data class RealVal(val value: Double) : BytecodeConst()
data class StringVal(val value: String) : BytecodeConst()
data class ObjRef(val value: Obj) : BytecodeConst()
}

View File

@ -0,0 +1,77 @@
/*
* 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
interface BytecodeDecoder {
fun readOpcode(code: ByteArray, ip: Int): Opcode
fun readSlot(code: ByteArray, ip: Int): Int
fun readConstId(code: ByteArray, ip: Int, width: Int): Int
fun readIp(code: ByteArray, ip: Int, width: Int): Int
}
object Decoder8 : BytecodeDecoder {
override fun readOpcode(code: ByteArray, ip: Int): Opcode =
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 readConstId(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
override fun readIp(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
}
object Decoder16 : BytecodeDecoder {
override fun readOpcode(code: ByteArray, ip: Int): Opcode =
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) or ((code[ip + 1].toInt() and 0xFF) shl 8)
override fun readConstId(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
override fun readIp(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
}
object Decoder32 : BytecodeDecoder {
override fun readOpcode(code: ByteArray, ip: Int): Opcode =
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 readConstId(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
override fun readIp(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
}
private fun readUInt(code: ByteArray, ip: Int, width: Int): Int {
var result = 0
var shift = 0
var idx = ip
var remaining = width
while (remaining-- > 0) {
result = result or ((code[idx].toInt() and 0xFF) shl shift)
shift += 8
idx++
}
return result
}

View File

@ -0,0 +1,130 @@
/*
* 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
object BytecodeDisassembler {
fun disassemble(fn: BytecodeFunction): String {
val decoder = when (fn.slotWidth) {
1 -> Decoder8
2 -> Decoder16
4 -> Decoder32
else -> error("Unsupported slot width: ${fn.slotWidth}")
}
val out = StringBuilder()
val code = fn.code
var ip = 0
while (ip < code.size) {
val op = decoder.readOpcode(code, ip)
val startIp = ip
ip += 1
val kinds = operandKinds(op)
val operands = ArrayList<String>(kinds.size)
for (kind in kinds) {
when (kind) {
OperandKind.SLOT -> {
val v = decoder.readSlot(code, ip)
ip += fn.slotWidth
operands += "s$v"
}
OperandKind.CONST -> {
val v = decoder.readConstId(code, ip, fn.constIdWidth)
ip += fn.constIdWidth
operands += "k$v"
}
OperandKind.IP -> {
val v = decoder.readIp(code, ip, fn.ipWidth)
ip += fn.ipWidth
operands += "ip$v"
}
OperandKind.COUNT -> {
val v = decoder.readConstId(code, ip, 2)
ip += 2
operands += "n$v"
}
OperandKind.ID -> {
val v = decoder.readConstId(code, ip, 2)
ip += 2
operands += "#$v"
}
}
}
out.append(startIp).append(": ").append(op.name)
if (operands.isNotEmpty()) {
out.append(' ').append(operands.joinToString(", "))
}
out.append('\n')
}
return out.toString()
}
private enum class OperandKind {
SLOT,
CONST,
IP,
COUNT,
ID,
}
private fun operandKinds(op: Opcode): List<OperandKind> {
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.CMP_NEQ_INT_REAL, Opcode.CMP_NEQ_REAL_INT,
Opcode.CMP_EQ_OBJ, Opcode.CMP_NEQ_OBJ, Opcode.CMP_REF_EQ_OBJ, Opcode.CMP_REF_NEQ_OBJ,
Opcode.CMP_LT_OBJ, Opcode.CMP_LTE_OBJ, Opcode.CMP_GT_OBJ, Opcode.CMP_GTE_OBJ,
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)
}
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.obj.Obj
import net.sergeych.lyng.obj.ObjNull
class BytecodeFrame(
val localCount: Int,
val argCount: Int,
) {
val slotCount: Int = localCount + argCount
val argBase: Int = localCount
private val slotTypes: ByteArray = ByteArray(slotCount) { SlotType.UNKNOWN.code }
private val objSlots: Array<Obj?> = arrayOfNulls(slotCount)
private val intSlots: LongArray = LongArray(slotCount)
private val realSlots: DoubleArray = DoubleArray(slotCount)
private val boolSlots: BooleanArray = BooleanArray(slotCount)
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) {
slotTypes[slot] = type.code
}
fun getObj(slot: Int): Obj = objSlots[slot] ?: ObjNull
fun setObj(slot: Int, value: Obj) {
objSlots[slot] = value
slotTypes[slot] = SlotType.OBJ.code
}
fun getInt(slot: Int): Long = intSlots[slot]
fun setInt(slot: Int, value: Long) {
intSlots[slot] = value
slotTypes[slot] = SlotType.INT.code
}
fun getReal(slot: Int): Double = realSlots[slot]
fun setReal(slot: Int, value: Double) {
realSlots[slot] = value
slotTypes[slot] = SlotType.REAL.code
}
fun getBool(slot: Int): Boolean = boolSlots[slot]
fun setBool(slot: Int, value: Boolean) {
boolSlots[slot] = value
slotTypes[slot] = SlotType.BOOL.code
}
fun clearSlot(slot: Int) {
slotTypes[slot] = SlotType.UNKNOWN.code
objSlots[slot] = null
intSlots[slot] = 0L
realSlots[slot] = 0.0
boolSlots[slot] = false
}
}

View File

@ -0,0 +1,34 @@
/*
* 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
data class BytecodeFunction(
val name: String,
val localCount: Int,
val slotWidth: Int,
val ipWidth: Int,
val constIdWidth: Int,
val constants: List<BytecodeConst>,
val fallbackStatements: List<net.sergeych.lyng.Statement>,
val code: ByteArray,
) {
init {
require(slotWidth == 1 || slotWidth == 2 || slotWidth == 4) { "slotWidth must be 1,2,4" }
require(ipWidth == 2 || ipWidth == 4) { "ipWidth must be 2 or 4" }
require(constIdWidth == 2 || constIdWidth == 4) { "constIdWidth must be 2 or 4" }
}
}

View File

@ -0,0 +1,704 @@
/*
* 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.Scope
import net.sergeych.lyng.obj.*
class BytecodeVm {
suspend fun execute(fn: BytecodeFunction, scope: Scope, args: List<Obj>): Obj {
val frame = BytecodeFrame(fn.localCount, args.size)
for (i in args.indices) {
frame.setObj(frame.argBase + i, args[i])
}
val decoder = when (fn.slotWidth) {
1 -> Decoder8
2 -> Decoder16
4 -> Decoder32
else -> error("Unsupported slot width: ${fn.slotWidth}")
}
var ip = 0
val code = fn.code
while (ip < code.size) {
val op = decoder.readOpcode(code, ip)
ip += 1
when (op) {
Opcode.NOP -> {
// 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_OBJ -> {
val constId = decoder.readConstId(code, ip, fn.constIdWidth)
ip += fn.constIdWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
when (val c = fn.constants[constId]) {
is BytecodeConst.ObjRef -> {
val obj = c.value
when (obj) {
is ObjInt -> frame.setInt(dst, obj.value)
is ObjReal -> frame.setReal(dst, obj.value)
is ObjBool -> frame.setBool(dst, obj.value)
else -> frame.setObj(dst, obj)
}
}
is BytecodeConst.StringVal -> frame.setObj(dst, ObjString(c.value))
else -> error("CONST_OBJ expects ObjRef/StringVal at $constId")
}
}
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.INT_TO_REAL -> {
val src = decoder.readSlot(code, ip)
ip += fn.slotWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
frame.setReal(dst, frame.getInt(src).toDouble())
}
Opcode.REAL_TO_INT -> {
val src = decoder.readSlot(code, ip)
ip += fn.slotWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
frame.setInt(dst, frame.getReal(src).toLong())
}
Opcode.BOOL_TO_INT -> {
val src = decoder.readSlot(code, ip)
ip += fn.slotWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
frame.setInt(dst, if (frame.getBool(src)) 1L else 0L)
}
Opcode.INT_TO_BOOL -> {
val src = decoder.readSlot(code, ip)
ip += fn.slotWidth
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
frame.setBool(dst, frame.getInt(src) != 0L)
}
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.SUB_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.MUL_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.DIV_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.MOD_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.NEG_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.INC_INT -> {
val slot = decoder.readSlot(code, ip)
ip += fn.slotWidth
frame.setInt(slot, frame.getInt(slot) + 1L)
}
Opcode.DEC_INT -> {
val slot = decoder.readSlot(code, ip)
ip += fn.slotWidth
frame.setInt(slot, frame.getInt(slot) - 1L)
}
Opcode.ADD_REAL -> {
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.setReal(dst, frame.getReal(a) + frame.getReal(b))
}
Opcode.SUB_REAL -> {
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.setReal(dst, frame.getReal(a) - frame.getReal(b))
}
Opcode.MUL_REAL -> {
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.setReal(dst, frame.getReal(a) * frame.getReal(b))
}
Opcode.DIV_REAL -> {
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.setReal(dst, frame.getReal(a) / frame.getReal(b))
}
Opcode.NEG_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.AND_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) and frame.getInt(b))
}
Opcode.OR_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) or frame.getInt(b))
}
Opcode.XOR_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) xor frame.getInt(b))
}
Opcode.SHL_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) shl frame.getInt(b).toInt())
}
Opcode.SHR_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) shr frame.getInt(b).toInt())
}
Opcode.USHR_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) ushr frame.getInt(b).toInt())
}
Opcode.INV_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).inv())
}
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_LTE_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_GT_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_GTE_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.CMP_NEQ_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_REAL -> {
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.getReal(a) == frame.getReal(b))
}
Opcode.CMP_NEQ_REAL -> {
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.getReal(a) != frame.getReal(b))
}
Opcode.CMP_LT_REAL -> {
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.getReal(a) < frame.getReal(b))
}
Opcode.CMP_LTE_REAL -> {
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.getReal(a) <= frame.getReal(b))
}
Opcode.CMP_GT_REAL -> {
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.getReal(a) > frame.getReal(b))
}
Opcode.CMP_GTE_REAL -> {
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.getReal(a) >= frame.getReal(b))
}
Opcode.CMP_EQ_BOOL -> {
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.getBool(a) == frame.getBool(b))
}
Opcode.CMP_NEQ_BOOL -> {
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.getBool(a) != frame.getBool(b))
}
Opcode.CMP_EQ_INT_REAL -> {
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).toDouble() == frame.getReal(b))
}
Opcode.CMP_EQ_REAL_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.getReal(a) == frame.getInt(b).toDouble())
}
Opcode.CMP_LT_INT_REAL -> {
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).toDouble() < frame.getReal(b))
}
Opcode.CMP_LT_REAL_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.getReal(a) < frame.getInt(b).toDouble())
}
Opcode.CMP_LTE_INT_REAL -> {
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).toDouble() <= frame.getReal(b))
}
Opcode.CMP_LTE_REAL_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.getReal(a) <= frame.getInt(b).toDouble())
}
Opcode.CMP_GT_INT_REAL -> {
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).toDouble() > frame.getReal(b))
}
Opcode.CMP_GT_REAL_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.getReal(a) > frame.getInt(b).toDouble())
}
Opcode.CMP_GTE_INT_REAL -> {
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).toDouble() >= frame.getReal(b))
}
Opcode.CMP_GTE_REAL_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.getReal(a) >= frame.getInt(b).toDouble())
}
Opcode.CMP_NEQ_INT_REAL -> {
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).toDouble() != frame.getReal(b))
}
Opcode.CMP_NEQ_REAL_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.getReal(a) != frame.getInt(b).toDouble())
}
Opcode.CMP_EQ_OBJ -> {
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.getObj(a).equals(scope, frame.getObj(b)))
}
Opcode.CMP_NEQ_OBJ -> {
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.getObj(a).equals(scope, frame.getObj(b)))
}
Opcode.CMP_REF_EQ_OBJ -> {
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.getObj(a) === frame.getObj(b))
}
Opcode.CMP_REF_NEQ_OBJ -> {
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.getObj(a) !== frame.getObj(b))
}
Opcode.CMP_LT_OBJ -> {
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.getObj(a).compareTo(scope, frame.getObj(b)) < 0)
}
Opcode.CMP_LTE_OBJ -> {
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.getObj(a).compareTo(scope, frame.getObj(b)) <= 0)
}
Opcode.CMP_GT_OBJ -> {
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.getObj(a).compareTo(scope, frame.getObj(b)) > 0)
}
Opcode.CMP_GTE_OBJ -> {
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.getObj(a).compareTo(scope, frame.getObj(b)) >= 0)
}
Opcode.NOT_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.AND_BOOL -> {
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.getBool(a) && frame.getBool(b))
}
Opcode.OR_BOOL -> {
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.getBool(a) || frame.getBool(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.JMP_IF_TRUE -> {
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.EVAL_FALLBACK -> {
val id = decoder.readConstId(code, ip, 2)
ip += 2
val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth
val stmt = fn.fallbackStatements.getOrNull(id)
?: error("Fallback statement not found: $id")
val result = stmt.execute(scope)
when (result) {
is ObjInt -> frame.setInt(dst, result.value)
is ObjReal -> frame.setReal(dst, result.value)
is ObjBool -> frame.setBool(dst, result.value)
else -> frame.setObj(dst, result)
}
}
Opcode.RET -> {
val slot = decoder.readSlot(code, ip)
return slotToObj(frame, slot)
}
Opcode.RET_VOID -> return ObjVoid
else -> error("Opcode not implemented: $op")
}
}
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

@ -0,0 +1,121 @@
/*
* 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
enum class Opcode(val code: Int) {
NOP(0x00),
MOVE_OBJ(0x01),
MOVE_INT(0x02),
MOVE_REAL(0x03),
MOVE_BOOL(0x04),
CONST_OBJ(0x05),
CONST_INT(0x06),
CONST_REAL(0x07),
CONST_BOOL(0x08),
CONST_NULL(0x09),
INT_TO_REAL(0x10),
REAL_TO_INT(0x11),
BOOL_TO_INT(0x12),
INT_TO_BOOL(0x13),
ADD_INT(0x20),
SUB_INT(0x21),
MUL_INT(0x22),
DIV_INT(0x23),
MOD_INT(0x24),
NEG_INT(0x25),
INC_INT(0x26),
DEC_INT(0x27),
ADD_REAL(0x30),
SUB_REAL(0x31),
MUL_REAL(0x32),
DIV_REAL(0x33),
NEG_REAL(0x34),
AND_INT(0x40),
OR_INT(0x41),
XOR_INT(0x42),
SHL_INT(0x43),
SHR_INT(0x44),
USHR_INT(0x45),
INV_INT(0x46),
CMP_EQ_INT(0x50),
CMP_NEQ_INT(0x51),
CMP_LT_INT(0x52),
CMP_LTE_INT(0x53),
CMP_GT_INT(0x54),
CMP_GTE_INT(0x55),
CMP_EQ_REAL(0x56),
CMP_NEQ_REAL(0x57),
CMP_LT_REAL(0x58),
CMP_LTE_REAL(0x59),
CMP_GT_REAL(0x5A),
CMP_GTE_REAL(0x5B),
CMP_EQ_BOOL(0x5C),
CMP_NEQ_BOOL(0x5D),
CMP_EQ_INT_REAL(0x60),
CMP_EQ_REAL_INT(0x61),
CMP_LT_INT_REAL(0x62),
CMP_LT_REAL_INT(0x63),
CMP_LTE_INT_REAL(0x64),
CMP_LTE_REAL_INT(0x65),
CMP_GT_INT_REAL(0x66),
CMP_GT_REAL_INT(0x67),
CMP_GTE_INT_REAL(0x68),
CMP_GTE_REAL_INT(0x69),
CMP_NEQ_INT_REAL(0x6A),
CMP_NEQ_REAL_INT(0x6B),
CMP_EQ_OBJ(0x6C),
CMP_NEQ_OBJ(0x6D),
CMP_REF_EQ_OBJ(0x6E),
CMP_REF_NEQ_OBJ(0x6F),
NOT_BOOL(0x70),
AND_BOOL(0x71),
OR_BOOL(0x72),
CMP_LT_OBJ(0x73),
CMP_LTE_OBJ(0x74),
CMP_GT_OBJ(0x75),
CMP_GTE_OBJ(0x76),
JMP(0x80),
JMP_IF_TRUE(0x81),
JMP_IF_FALSE(0x82),
RET(0x83),
RET_VOID(0x84),
CALL_DIRECT(0x90),
CALL_VIRTUAL(0x91),
CALL_FALLBACK(0x92),
GET_FIELD(0xA0),
SET_FIELD(0xA1),
GET_INDEX(0xA2),
SET_INDEX(0xA3),
EVAL_FALLBACK(0xB0),
;
companion object {
private val byCode: Map<Int, Opcode> = values().associateBy { it.code }
fun fromCode(code: Int): Opcode? = byCode[code]
}
}

View File

@ -0,0 +1,25 @@
/*
* 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
enum class SlotType(val code: Byte) {
UNKNOWN(0),
OBJ(1),
INT(2),
REAL(3),
BOOL(4),
}

View File

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

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,30 @@ 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 ToBoolStatement(
val expr: Statement,
override val pos: Pos,
) : Statement() {
override suspend fun execute(scope: Scope): Obj {
return if (expr.execute(scope).toBool()) net.sergeych.lyng.obj.ObjTrue else net.sergeych.lyng.obj.ObjFalse
}
}
class ExpressionStatement(
val ref: net.sergeych.lyng.obj.ObjRef,
override val pos: Pos

View File

@ -0,0 +1,301 @@
/*
* 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.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.LocalSlotRef
import net.sergeych.lyng.obj.ObjFalse
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjTrue
import net.sergeych.lyng.obj.ObjReal
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.AssignRef
import net.sergeych.lyng.obj.ValueFnRef
import net.sergeych.lyng.obj.ObjVoid
import net.sergeych.lyng.obj.toBool
import net.sergeych.lyng.obj.toDouble
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())
}
@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())
}
@Test
fun ifWithoutElseReturnsVoid() = kotlinx.coroutines.test.runTest {
val cond = ExpressionStatement(
BinaryOpRef(
BinOp.LT,
ConstRef(ObjInt.of(2).asReadonly),
ConstRef(ObjInt.of(1).asReadonly),
),
net.sergeych.lyng.Pos.builtIn
)
val thenStmt = ExpressionStatement(
ConstRef(ObjInt.of(10).asReadonly),
net.sergeych.lyng.Pos.builtIn
)
val ifStmt = IfStatement(cond, thenStmt, null, net.sergeych.lyng.Pos.builtIn)
val fn = BytecodeCompiler().compileStatement("ifNoElse", ifStmt).also {
if (it == null) {
error("bytecode compile failed for ifNoElse")
}
}!!
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(ObjVoid, result)
}
@Test
fun andIsShortCircuit() = kotlinx.coroutines.test.runTest {
val throwingRef = ValueFnRef { error("should not execute") }
val expr = ExpressionStatement(
BinaryOpRef(
BinOp.AND,
ConstRef(ObjFalse.asReadonly),
throwingRef
),
net.sergeych.lyng.Pos.builtIn
)
val fn = BytecodeCompiler().compileExpression("andShort", expr) ?: error("bytecode compile failed")
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(false, result.toBool())
}
@Test
fun orIsShortCircuit() = kotlinx.coroutines.test.runTest {
val throwingRef = ValueFnRef { error("should not execute") }
val expr = ExpressionStatement(
BinaryOpRef(
BinOp.OR,
ConstRef(ObjTrue.asReadonly),
throwingRef
),
net.sergeych.lyng.Pos.builtIn
)
val fn = BytecodeCompiler().compileExpression("orShort", expr) ?: error("bytecode compile failed")
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(true, result.toBool())
}
@Test
fun realArithmeticUsesBytecodeOps() = kotlinx.coroutines.test.runTest {
val expr = ExpressionStatement(
BinaryOpRef(
BinOp.PLUS,
ConstRef(ObjReal.of(2.5).asReadonly),
ConstRef(ObjReal.of(3.25).asReadonly),
),
net.sergeych.lyng.Pos.builtIn
)
val fn = BytecodeCompiler().compileExpression("realPlus", expr) ?: error("bytecode compile failed")
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(5.75, result.toDouble())
}
@Test
fun mixedIntRealComparisonUsesBytecodeOps() = kotlinx.coroutines.test.runTest {
val ltExpr = ExpressionStatement(
BinaryOpRef(
BinOp.LT,
ConstRef(ObjInt.of(2).asReadonly),
ConstRef(ObjReal.of(2.5).asReadonly),
),
net.sergeych.lyng.Pos.builtIn
)
val ltFn = BytecodeCompiler().compileExpression("mixedLt", ltExpr) ?: error("bytecode compile failed")
val ltResult = BytecodeVm().execute(ltFn, Scope(), emptyList())
assertEquals(true, ltResult.toBool())
val eqExpr = ExpressionStatement(
BinaryOpRef(
BinOp.EQ,
ConstRef(ObjReal.of(4.0).asReadonly),
ConstRef(ObjInt.of(4).asReadonly),
),
net.sergeych.lyng.Pos.builtIn
)
val eqFn = BytecodeCompiler().compileExpression("mixedEq", eqExpr) ?: error("bytecode compile failed")
val eqResult = BytecodeVm().execute(eqFn, Scope(), emptyList())
assertEquals(true, eqResult.toBool())
}
@Test
fun mixedIntRealArithmeticUsesBytecodeOps() = kotlinx.coroutines.test.runTest {
val expr = ExpressionStatement(
BinaryOpRef(
BinOp.PLUS,
ConstRef(ObjInt.of(2).asReadonly),
ConstRef(ObjReal.of(3.5).asReadonly),
),
net.sergeych.lyng.Pos.builtIn
)
val fn = BytecodeCompiler().compileExpression("mixedPlus", expr) ?: error("bytecode compile failed")
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(5.5, result.toDouble())
}
@Test
fun mixedIntRealNotEqualUsesBytecodeOps() = kotlinx.coroutines.test.runTest {
val expr = ExpressionStatement(
BinaryOpRef(
BinOp.NEQ,
ConstRef(ObjInt.of(3).asReadonly),
ConstRef(ObjReal.of(2.5).asReadonly),
),
net.sergeych.lyng.Pos.builtIn
)
val fn = BytecodeCompiler().compileExpression("mixedNeq", expr) ?: error("bytecode compile failed")
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(true, result.toBool())
}
@Test
fun localSlotTypeTrackingEnablesArithmetic() = kotlinx.coroutines.test.runTest {
val slotRef = LocalSlotRef("a", 0, 0, net.sergeych.lyng.Pos.builtIn)
val assign = AssignRef(
slotRef,
ConstRef(ObjInt.of(2).asReadonly),
net.sergeych.lyng.Pos.builtIn
)
val expr = ExpressionStatement(
BinaryOpRef(
BinOp.PLUS,
assign,
slotRef
),
net.sergeych.lyng.Pos.builtIn
)
val fn = BytecodeCompiler().compileExpression("localSlotAdd", expr) ?: error("bytecode compile failed")
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(4, result.toInt())
}
@Test
fun objectEqualityUsesBytecodeOps() = kotlinx.coroutines.test.runTest {
val expr = ExpressionStatement(
BinaryOpRef(
BinOp.EQ,
ConstRef(ObjString("abc").asReadonly),
ConstRef(ObjString("abc").asReadonly),
),
net.sergeych.lyng.Pos.builtIn
)
val fn = BytecodeCompiler().compileExpression("objEq", expr) ?: error("bytecode compile failed")
val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(true, result.toBool())
}
@Test
fun objectReferenceEqualityUsesBytecodeOps() = kotlinx.coroutines.test.runTest {
val shared = ObjList()
val eqExpr = ExpressionStatement(
BinaryOpRef(
BinOp.REF_EQ,
ConstRef(shared.asReadonly),
ConstRef(shared.asReadonly),
),
net.sergeych.lyng.Pos.builtIn
)
val eqFn = BytecodeCompiler().compileExpression("objRefEq", eqExpr) ?: error("bytecode compile failed")
val eqResult = BytecodeVm().execute(eqFn, Scope(), emptyList())
assertEquals(true, eqResult.toBool())
val neqExpr = ExpressionStatement(
BinaryOpRef(
BinOp.REF_NEQ,
ConstRef(ObjList().asReadonly),
ConstRef(ObjList().asReadonly),
),
net.sergeych.lyng.Pos.builtIn
)
val neqFn = BytecodeCompiler().compileExpression("objRefNeq", neqExpr) ?: error("bytecode compile failed")
val neqResult = BytecodeVm().execute(neqFn, Scope(), emptyList())
assertEquals(true, neqResult.toBool())
}
@Test
fun objectComparisonUsesBytecodeOps() = kotlinx.coroutines.test.runTest {
val ltExpr = ExpressionStatement(
BinaryOpRef(
BinOp.LT,
ConstRef(ObjString("a").asReadonly),
ConstRef(ObjString("b").asReadonly),
),
net.sergeych.lyng.Pos.builtIn
)
val ltFn = BytecodeCompiler().compileExpression("objLt", ltExpr) ?: error("bytecode compile failed")
val ltResult = BytecodeVm().execute(ltFn, Scope(), emptyList())
assertEquals(true, ltResult.toBool())
val gteExpr = ExpressionStatement(
BinaryOpRef(
BinOp.GTE,
ConstRef(ObjString("b").asReadonly),
ConstRef(ObjString("a").asReadonly),
),
net.sergeych.lyng.Pos.builtIn
)
val gteFn = BytecodeCompiler().compileExpression("objGte", gteExpr) ?: error("bytecode compile failed")
val gteResult = BytecodeVm().execute(gteFn, Scope(), emptyList())
assertEquals(true, gteResult.toBool())
}
}