Add minimal bytecode compiler scaffold

This commit is contained in:
Sergey Chernov 2026-01-25 17:09:02 +03:00
parent f42ea0a04c
commit d8b00a805c
3 changed files with 417 additions and 6 deletions

View File

@ -0,0 +1,144 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.bytecode
class BytecodeBuilder {
data class Instr(val op: Opcode, val operands: IntArray)
private val instructions = mutableListOf<Instr>()
private val constPool = mutableListOf<BytecodeConst>()
fun addConst(c: BytecodeConst): Int {
constPool += c
return constPool.lastIndex
}
fun emit(op: Opcode, vararg operands: Int) {
instructions += Instr(op, operands.copyOf())
}
fun build(name: String, localCount: Int): BytecodeFunction {
val slotWidth = when {
localCount < 256 -> 1
localCount < 65536 -> 2
else -> 4
}
val constIdWidth = if (constPool.size < 65536) 2 else 4
val ipWidth = 2
val code = ByteArrayOutput()
for (ins in instructions) {
code.writeU8(ins.op.code.toInt() and 0xFF)
val kinds = operandKinds(ins.op)
if (kinds.size != ins.operands.size) {
error("Operand count mismatch for ${ins.op}: expected ${kinds.size}, got ${ins.operands.size}")
}
for (i in kinds.indices) {
val v = ins.operands[i]
when (kinds[i]) {
OperandKind.SLOT -> code.writeUInt(v, slotWidth)
OperandKind.CONST -> code.writeUInt(v, constIdWidth)
OperandKind.IP -> code.writeUInt(v, ipWidth)
OperandKind.COUNT -> code.writeUInt(v, 2)
OperandKind.ID -> code.writeUInt(v, 2)
}
}
}
return BytecodeFunction(
name = name,
localCount = localCount,
slotWidth = slotWidth,
ipWidth = ipWidth,
constIdWidth = constIdWidth,
constants = constPool.toList(),
code = code.toByteArray()
)
}
private fun operandKinds(op: Opcode): List<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.AND_BOOL, Opcode.OR_BOOL ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET ->
listOf(OperandKind.SLOT)
Opcode.JMP ->
listOf(OperandKind.IP)
Opcode.JMP_IF_TRUE, Opcode.JMP_IF_FALSE ->
listOf(OperandKind.SLOT, OperandKind.IP)
Opcode.CALL_DIRECT, Opcode.CALL_FALLBACK ->
listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.CALL_VIRTUAL ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT, OperandKind.COUNT, OperandKind.SLOT)
Opcode.GET_FIELD ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT)
Opcode.SET_FIELD ->
listOf(OperandKind.SLOT, OperandKind.ID, OperandKind.SLOT)
Opcode.GET_INDEX ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.SET_INDEX ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.EVAL_FALLBACK ->
listOf(OperandKind.ID, OperandKind.SLOT)
}
}
private enum class OperandKind {
SLOT,
CONST,
IP,
COUNT,
ID,
}
private class ByteArrayOutput {
private val data = ArrayList<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,267 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.bytecode
import net.sergeych.lyng.ExpressionStatement
import net.sergeych.lyng.obj.*
class BytecodeCompiler {
private val builder = BytecodeBuilder()
private var nextSlot = 0
fun compileExpression(name: String, stmt: ExpressionStatement): BytecodeFunction? {
val value = compileRef(stmt.ref) ?: return null
builder.emit(Opcode.RET, value.slot)
val localCount = maxOf(nextSlot, value.slot + 1)
return builder.build(name, localCount)
}
private data class CompiledValue(val slot: Int, val type: SlotType)
private fun allocSlot(): Int = nextSlot++
private fun compileRef(ref: ObjRef): CompiledValue? {
return when (ref) {
is ConstRef -> compileConst(ref.constValue)
is LocalSlotRef -> {
if (ref.name.isEmpty()) return null
if (refDepth(ref) != 0) return null
CompiledValue(refSlot(ref), SlotType.UNKNOWN)
}
is BinaryOpRef -> compileBinary(ref)
is UnaryOpRef -> compileUnary(ref)
is AssignRef -> compileAssign(ref)
else -> null
}
}
private fun compileConst(obj: Obj): CompiledValue? {
val slot = allocSlot()
when (obj) {
is ObjInt -> {
val id = builder.addConst(BytecodeConst.IntVal(obj.value))
builder.emit(Opcode.CONST_INT, id, slot)
return CompiledValue(slot, SlotType.INT)
}
is ObjReal -> {
val id = builder.addConst(BytecodeConst.RealVal(obj.value))
builder.emit(Opcode.CONST_REAL, id, slot)
return CompiledValue(slot, SlotType.REAL)
}
is ObjBool -> {
val id = builder.addConst(BytecodeConst.Bool(obj.value))
builder.emit(Opcode.CONST_BOOL, id, slot)
return CompiledValue(slot, SlotType.BOOL)
}
is ObjString -> {
val id = builder.addConst(BytecodeConst.StringVal(obj.value))
builder.emit(Opcode.CONST_OBJ, id, slot)
return CompiledValue(slot, SlotType.OBJ)
}
ObjNull -> {
builder.emit(Opcode.CONST_NULL, slot)
return CompiledValue(slot, SlotType.OBJ)
}
else -> {
val id = builder.addConst(BytecodeConst.ObjRef(obj))
builder.emit(Opcode.CONST_OBJ, id, slot)
return CompiledValue(slot, SlotType.OBJ)
}
}
}
private fun compileUnary(ref: UnaryOpRef): CompiledValue? {
val a = compileRef(unaryOperand(ref)) ?: return null
val out = allocSlot()
return when (unaryOp(ref)) {
UnaryOp.NEGATE -> when (a.type) {
SlotType.INT -> {
builder.emit(Opcode.NEG_INT, a.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.REAL -> {
builder.emit(Opcode.NEG_REAL, a.slot, out)
CompiledValue(out, SlotType.REAL)
}
else -> null
}
UnaryOp.NOT -> {
if (a.type != SlotType.BOOL) return null
builder.emit(Opcode.NOT_BOOL, a.slot, out)
CompiledValue(out, SlotType.BOOL)
}
UnaryOp.BITNOT -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.INV_INT, a.slot, out)
CompiledValue(out, SlotType.INT)
}
}
}
private fun compileBinary(ref: BinaryOpRef): CompiledValue? {
val a = compileRef(binaryLeft(ref)) ?: return null
val b = compileRef(binaryRight(ref)) ?: return null
if (a.type != b.type && a.type != SlotType.UNKNOWN && b.type != SlotType.UNKNOWN) return null
val out = allocSlot()
val op = binaryOp(ref)
return when (op) {
BinOp.PLUS -> when (a.type) {
SlotType.INT -> {
builder.emit(Opcode.ADD_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.REAL -> {
builder.emit(Opcode.ADD_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.REAL)
}
else -> null
}
BinOp.MINUS -> when (a.type) {
SlotType.INT -> {
builder.emit(Opcode.SUB_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.REAL -> {
builder.emit(Opcode.SUB_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.REAL)
}
else -> null
}
BinOp.STAR -> when (a.type) {
SlotType.INT -> {
builder.emit(Opcode.MUL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.REAL -> {
builder.emit(Opcode.MUL_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.REAL)
}
else -> null
}
BinOp.SLASH -> when (a.type) {
SlotType.INT -> {
builder.emit(Opcode.DIV_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.REAL -> {
builder.emit(Opcode.DIV_REAL, a.slot, b.slot, out)
CompiledValue(out, SlotType.REAL)
}
else -> null
}
BinOp.PERCENT -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.MOD_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
BinOp.EQ -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.CMP_EQ_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
BinOp.NEQ -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.CMP_NEQ_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
BinOp.LT -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.CMP_LT_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
BinOp.LTE -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.CMP_LTE_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
BinOp.GT -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.CMP_GT_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
BinOp.GTE -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.CMP_GTE_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL)
}
BinOp.BAND -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.AND_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
BinOp.BOR -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.OR_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
BinOp.BXOR -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.XOR_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
BinOp.SHL -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.SHL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
BinOp.SHR -> {
if (a.type != SlotType.INT) return null
builder.emit(Opcode.SHR_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.INT)
}
else -> null
}
}
private fun compileAssign(ref: AssignRef): CompiledValue? {
val target = assignTarget(ref) ?: return null
if (refDepth(target) != 0) return null
val value = compileRef(assignValue(ref)) ?: return null
val slot = refSlot(target)
when (value.type) {
SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, slot)
SlotType.REAL -> builder.emit(Opcode.MOVE_REAL, value.slot, slot)
SlotType.BOOL -> builder.emit(Opcode.MOVE_BOOL, value.slot, slot)
else -> builder.emit(Opcode.MOVE_OBJ, value.slot, slot)
}
return CompiledValue(slot, value.type)
}
private fun refSlot(ref: LocalSlotRef): Int = ref.slot
private fun refDepth(ref: LocalSlotRef): Int = refDepthAccessor(ref)
private fun binaryLeft(ref: BinaryOpRef): ObjRef = binaryLeftAccessor(ref)
private fun binaryRight(ref: BinaryOpRef): ObjRef = binaryRightAccessor(ref)
private fun binaryOp(ref: BinaryOpRef): BinOp = binaryOpAccessor(ref)
private fun unaryOperand(ref: UnaryOpRef): ObjRef = unaryOperandAccessor(ref)
private fun unaryOp(ref: UnaryOpRef): UnaryOp = unaryOpAccessor(ref)
private fun assignTarget(ref: AssignRef): LocalSlotRef? = assignTargetAccessor(ref)
private fun assignValue(ref: AssignRef): ObjRef = assignValueAccessor(ref)
// Accessor helpers to avoid exposing fields directly in ObjRef classes.
private fun refDepthAccessor(ref: LocalSlotRef): Int = ref.depth
private fun binaryLeftAccessor(ref: BinaryOpRef): ObjRef = ref.left
private fun binaryRightAccessor(ref: BinaryOpRef): ObjRef = ref.right
private fun binaryOpAccessor(ref: BinaryOpRef): BinOp = ref.op
private fun unaryOperandAccessor(ref: UnaryOpRef): ObjRef = ref.a
private fun unaryOpAccessor(ref: UnaryOpRef): UnaryOp = ref.op
private fun assignTargetAccessor(ref: AssignRef): LocalSlotRef? = ref.target as? LocalSlotRef
private fun assignValueAccessor(ref: AssignRef): ObjRef = ref.value
}

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 {