Add bytecode if support and test
This commit is contained in:
parent
ea877748e5
commit
6560457e3d
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,10 +17,19 @@
|
||||
package net.sergeych.lyng.bytecode
|
||||
|
||||
class BytecodeBuilder {
|
||||
data class Instr(val op: Opcode, val operands: IntArray)
|
||||
sealed interface Operand {
|
||||
data class IntVal(val value: Int) : Operand
|
||||
data class LabelRef(val label: Label) : Operand
|
||||
}
|
||||
|
||||
data class Label(val id: Int)
|
||||
|
||||
data class Instr(val op: Opcode, val operands: List<Operand>)
|
||||
|
||||
private val instructions = mutableListOf<Instr>()
|
||||
private val constPool = mutableListOf<BytecodeConst>()
|
||||
private val labelPositions = mutableMapOf<Label, Int>()
|
||||
private var nextLabelId = 0
|
||||
|
||||
fun addConst(c: BytecodeConst): Int {
|
||||
constPool += c
|
||||
@ -28,7 +37,17 @@ class BytecodeBuilder {
|
||||
}
|
||||
|
||||
fun emit(op: Opcode, vararg operands: Int) {
|
||||
instructions += Instr(op, operands.copyOf())
|
||||
instructions += Instr(op, operands.map { Operand.IntVal(it) })
|
||||
}
|
||||
|
||||
fun emit(op: Opcode, operands: List<Operand>) {
|
||||
instructions += Instr(op, operands)
|
||||
}
|
||||
|
||||
fun label(): Label = Label(nextLabelId++)
|
||||
|
||||
fun mark(label: Label) {
|
||||
labelPositions[label] = instructions.size
|
||||
}
|
||||
|
||||
fun build(name: String, localCount: Int): BytecodeFunction {
|
||||
@ -39,15 +58,32 @@ class BytecodeBuilder {
|
||||
}
|
||||
val constIdWidth = if (constPool.size < 65536) 2 else 4
|
||||
val ipWidth = 2
|
||||
val instrOffsets = IntArray(instructions.size)
|
||||
var currentIp = 0
|
||||
for (i in instructions.indices) {
|
||||
instrOffsets[i] = currentIp
|
||||
val kinds = operandKinds(instructions[i].op)
|
||||
currentIp += 1 + kinds.sumOf { operandWidth(it, slotWidth, constIdWidth, ipWidth) }
|
||||
}
|
||||
val labelIps = mutableMapOf<Label, Int>()
|
||||
for ((label, idx) in labelPositions) {
|
||||
labelIps[label] = instrOffsets.getOrNull(idx) ?: error("Invalid label index: $idx")
|
||||
}
|
||||
|
||||
val code = ByteArrayOutput()
|
||||
for (ins in instructions) {
|
||||
code.writeU8(ins.op.code.toInt() and 0xFF)
|
||||
code.writeU8(ins.op.code and 0xFF)
|
||||
val kinds = operandKinds(ins.op)
|
||||
if (kinds.size != ins.operands.size) {
|
||||
error("Operand count mismatch for ${ins.op}: expected ${kinds.size}, got ${ins.operands.size}")
|
||||
}
|
||||
for (i in kinds.indices) {
|
||||
val v = ins.operands[i]
|
||||
val operand = ins.operands[i]
|
||||
val v = when (operand) {
|
||||
is Operand.IntVal -> operand.value
|
||||
is Operand.LabelRef -> labelIps[operand.label]
|
||||
?: error("Unknown label ${operand.label.id} for ${ins.op}")
|
||||
}
|
||||
when (kinds[i]) {
|
||||
OperandKind.SLOT -> code.writeUInt(v, slotWidth)
|
||||
OperandKind.CONST -> code.writeUInt(v, constIdWidth)
|
||||
@ -115,6 +151,16 @@ class BytecodeBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
private fun operandWidth(kind: OperandKind, slotWidth: Int, constIdWidth: Int, ipWidth: Int): Int {
|
||||
return when (kind) {
|
||||
OperandKind.SLOT -> slotWidth
|
||||
OperandKind.CONST -> constIdWidth
|
||||
OperandKind.IP -> ipWidth
|
||||
OperandKind.COUNT -> 2
|
||||
OperandKind.ID -> 2
|
||||
}
|
||||
}
|
||||
|
||||
private enum class OperandKind {
|
||||
SLOT,
|
||||
CONST,
|
||||
|
||||
@ -17,12 +17,21 @@
|
||||
package net.sergeych.lyng.bytecode
|
||||
|
||||
import net.sergeych.lyng.ExpressionStatement
|
||||
import net.sergeych.lyng.IfStatement
|
||||
import net.sergeych.lyng.obj.*
|
||||
|
||||
class BytecodeCompiler {
|
||||
private val builder = BytecodeBuilder()
|
||||
private var nextSlot = 0
|
||||
|
||||
fun compileStatement(name: String, stmt: net.sergeych.lyng.Statement): BytecodeFunction? {
|
||||
return when (stmt) {
|
||||
is ExpressionStatement -> compileExpression(name, stmt)
|
||||
is net.sergeych.lyng.IfStatement -> compileIf(name, stmt)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun compileExpression(name: String, stmt: ExpressionStatement): BytecodeFunction? {
|
||||
val value = compileRef(stmt.ref) ?: return null
|
||||
builder.emit(Opcode.RET, value.slot)
|
||||
@ -241,27 +250,61 @@ class BytecodeCompiler {
|
||||
return CompiledValue(slot, value.type)
|
||||
}
|
||||
|
||||
private fun refSlot(ref: LocalSlotRef): Int = ref.slot
|
||||
private fun compileIf(name: String, stmt: IfStatement): BytecodeFunction? {
|
||||
val conditionStmt = stmt.condition as? ExpressionStatement ?: return null
|
||||
val condValue = compileRef(conditionStmt.ref) ?: return null
|
||||
if (condValue.type != SlotType.BOOL) return null
|
||||
|
||||
private fun refDepth(ref: LocalSlotRef): Int = refDepthAccessor(ref)
|
||||
val resultSlot = allocSlot()
|
||||
val elseLabel = builder.label()
|
||||
val endLabel = builder.label()
|
||||
|
||||
private fun binaryLeft(ref: BinaryOpRef): ObjRef = binaryLeftAccessor(ref)
|
||||
private fun binaryRight(ref: BinaryOpRef): ObjRef = binaryRightAccessor(ref)
|
||||
private fun binaryOp(ref: BinaryOpRef): BinOp = binaryOpAccessor(ref)
|
||||
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)))
|
||||
|
||||
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
|
||||
builder.mark(elseLabel)
|
||||
if (stmt.elseBody != null) {
|
||||
val elseValue = compileStatementValue(stmt.elseBody) ?: return null
|
||||
emitMove(elseValue, resultSlot)
|
||||
} else {
|
||||
val id = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
|
||||
builder.emit(Opcode.CONST_OBJ, id, resultSlot)
|
||||
}
|
||||
|
||||
builder.mark(endLabel)
|
||||
builder.emit(Opcode.RET, resultSlot)
|
||||
val localCount = maxOf(nextSlot, resultSlot + 1)
|
||||
return builder.build(name, localCount)
|
||||
}
|
||||
|
||||
private fun compileStatementValue(stmt: net.sergeych.lyng.Statement): CompiledValue? {
|
||||
return when (stmt) {
|
||||
is ExpressionStatement -> compileRef(stmt.ref)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitMove(value: CompiledValue, dstSlot: Int) {
|
||||
when (value.type) {
|
||||
SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, dstSlot)
|
||||
SlotType.REAL -> builder.emit(Opcode.MOVE_REAL, value.slot, dstSlot)
|
||||
SlotType.BOOL -> builder.emit(Opcode.MOVE_BOOL, value.slot, dstSlot)
|
||||
else -> builder.emit(Opcode.MOVE_OBJ, value.slot, dstSlot)
|
||||
}
|
||||
}
|
||||
|
||||
private fun refSlot(ref: LocalSlotRef): Int = ref.slot
|
||||
private fun refDepth(ref: LocalSlotRef): Int = 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
|
||||
}
|
||||
|
||||
@ -109,6 +109,37 @@ class BytecodeVm {
|
||||
ip += fn.slotWidth
|
||||
frame.setInt(dst, frame.getInt(a) + frame.getInt(b))
|
||||
}
|
||||
Opcode.CMP_LT_INT -> {
|
||||
val a = decoder.readSlot(code, ip)
|
||||
ip += fn.slotWidth
|
||||
val b = decoder.readSlot(code, ip)
|
||||
ip += fn.slotWidth
|
||||
val dst = decoder.readSlot(code, ip)
|
||||
ip += fn.slotWidth
|
||||
frame.setBool(dst, frame.getInt(a) < frame.getInt(b))
|
||||
}
|
||||
Opcode.CMP_EQ_INT -> {
|
||||
val a = decoder.readSlot(code, ip)
|
||||
ip += fn.slotWidth
|
||||
val b = decoder.readSlot(code, ip)
|
||||
ip += fn.slotWidth
|
||||
val dst = decoder.readSlot(code, ip)
|
||||
ip += fn.slotWidth
|
||||
frame.setBool(dst, frame.getInt(a) == frame.getInt(b))
|
||||
}
|
||||
Opcode.JMP -> {
|
||||
val target = decoder.readIp(code, ip, fn.ipWidth)
|
||||
ip = target
|
||||
}
|
||||
Opcode.JMP_IF_FALSE -> {
|
||||
val cond = decoder.readSlot(code, ip)
|
||||
ip += fn.slotWidth
|
||||
val target = decoder.readIp(code, ip, fn.ipWidth)
|
||||
ip += fn.ipWidth
|
||||
if (!frame.getBool(cond)) {
|
||||
ip = target
|
||||
}
|
||||
}
|
||||
Opcode.RET -> {
|
||||
val slot = decoder.readSlot(code, ip)
|
||||
return slotToObj(frame, slot)
|
||||
|
||||
@ -20,6 +20,7 @@ package net.sergeych.lyng
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
import net.sergeych.lyng.obj.toBool
|
||||
|
||||
fun String.toSource(name: String = "eval"): Source = Source(name, this)
|
||||
|
||||
@ -63,6 +64,21 @@ abstract class Statement(
|
||||
|
||||
}
|
||||
|
||||
class IfStatement(
|
||||
val condition: Statement,
|
||||
val ifBody: Statement,
|
||||
val elseBody: Statement?,
|
||||
override val pos: Pos,
|
||||
) : Statement() {
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
return if (condition.execute(scope).toBool()) {
|
||||
ifBody.execute(scope)
|
||||
} else {
|
||||
elseBody?.execute(scope) ?: ObjVoid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExpressionStatement(
|
||||
val ref: net.sergeych.lyng.obj.ObjRef,
|
||||
override val pos: Pos
|
||||
|
||||
@ -14,11 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import net.sergeych.lyng.ExpressionStatement
|
||||
import net.sergeych.lyng.IfStatement
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.bytecode.BytecodeBuilder
|
||||
import net.sergeych.lyng.bytecode.BytecodeCompiler
|
||||
import net.sergeych.lyng.bytecode.BytecodeConst
|
||||
import net.sergeych.lyng.bytecode.BytecodeVm
|
||||
import net.sergeych.lyng.bytecode.Opcode
|
||||
import net.sergeych.lyng.obj.BinaryOpRef
|
||||
import net.sergeych.lyng.obj.BinOp
|
||||
import net.sergeych.lyng.obj.ConstRef
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.toInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -37,4 +44,28 @@ class BytecodeVmTest {
|
||||
val result = BytecodeVm().execute(fn, Scope(), emptyList())
|
||||
assertEquals(5, result.toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ifExpressionReturnsThenValue() = kotlinx.coroutines.test.runTest {
|
||||
val cond = ExpressionStatement(
|
||||
BinaryOpRef(
|
||||
BinOp.LT,
|
||||
ConstRef(ObjInt.of(2).asReadonly),
|
||||
ConstRef(ObjInt.of(3).asReadonly),
|
||||
),
|
||||
net.sergeych.lyng.Pos.builtIn
|
||||
)
|
||||
val thenStmt = ExpressionStatement(
|
||||
ConstRef(ObjInt.of(10).asReadonly),
|
||||
net.sergeych.lyng.Pos.builtIn
|
||||
)
|
||||
val elseStmt = ExpressionStatement(
|
||||
ConstRef(ObjInt.of(20).asReadonly),
|
||||
net.sergeych.lyng.Pos.builtIn
|
||||
)
|
||||
val ifStmt = IfStatement(cond, thenStmt, elseStmt, net.sergeych.lyng.Pos.builtIn)
|
||||
val fn = BytecodeCompiler().compileStatement("ifTest", ifStmt) ?: error("bytecode compile failed")
|
||||
val result = BytecodeVm().execute(fn, Scope(), emptyList())
|
||||
assertEquals(10, result.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user