Add object equality and reference ops to bytecode

This commit is contained in:
Sergey Chernov 2026-01-25 21:33:28 +03:00
parent 9c56cf751b
commit fd1548c86c
7 changed files with 121 additions and 8 deletions

View File

@ -151,6 +151,10 @@ Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass.
- CMP_GTE_REAL_INT S, S -> S - CMP_GTE_REAL_INT S, S -> S
- CMP_NEQ_INT_REAL S, S -> S - CMP_NEQ_INT_REAL S, S -> S
- CMP_NEQ_REAL_INT 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
### Boolean ops ### Boolean ops
- NOT_BOOL S -> S - NOT_BOOL S -> S

View File

@ -133,6 +133,7 @@ class BytecodeBuilder {
Opcode.CMP_EQ_INT_REAL, Opcode.CMP_EQ_REAL_INT, Opcode.CMP_LT_INT_REAL, Opcode.CMP_LT_REAL_INT, 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_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_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.AND_BOOL, Opcode.OR_BOOL -> Opcode.AND_BOOL, Opcode.OR_BOOL ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET -> Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET ->

View File

@ -253,6 +253,16 @@ class BytecodeCompiler {
BinOp.GTE -> { BinOp.GTE -> {
compileCompareGte(a, b, out) 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 -> { BinOp.AND -> {
if (a.type != SlotType.BOOL) return null if (a.type != SlotType.BOOL) return null
builder.emit(Opcode.AND_BOOL, a.slot, b.slot, out) builder.emit(Opcode.AND_BOOL, a.slot, b.slot, out)
@ -336,6 +346,10 @@ class BytecodeCompiler {
builder.emit(Opcode.CMP_EQ_REAL_INT, a.slot, b.slot, out) builder.emit(Opcode.CMP_EQ_REAL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL) 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 else -> null
} }
} }
@ -363,6 +377,10 @@ class BytecodeCompiler {
builder.emit(Opcode.CMP_NEQ_REAL_INT, a.slot, b.slot, out) builder.emit(Opcode.CMP_NEQ_REAL_INT, a.slot, b.slot, out)
CompiledValue(out, SlotType.BOOL) 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 else -> null
} }
} }

View File

@ -101,6 +101,7 @@ object BytecodeDisassembler {
Opcode.CMP_EQ_INT_REAL, Opcode.CMP_EQ_REAL_INT, Opcode.CMP_LT_INT_REAL, Opcode.CMP_LT_REAL_INT, 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_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_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.AND_BOOL, Opcode.OR_BOOL -> Opcode.AND_BOOL, Opcode.OR_BOOL ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT) listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET -> Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET ->

View File

@ -72,8 +72,8 @@ class BytecodeVm {
ip += fn.constIdWidth ip += fn.constIdWidth
val dst = decoder.readSlot(code, ip) val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth ip += fn.slotWidth
val c = fn.constants[constId] as? BytecodeConst.ObjRef when (val c = fn.constants[constId]) {
?: error("CONST_OBJ expects ObjRef at $constId") is BytecodeConst.ObjRef -> {
val obj = c.value val obj = c.value
when (obj) { when (obj) {
is ObjInt -> frame.setInt(dst, obj.value) is ObjInt -> frame.setInt(dst, obj.value)
@ -82,6 +82,10 @@ class BytecodeVm {
else -> frame.setObj(dst, obj) 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 -> { Opcode.CONST_NULL -> {
val dst = decoder.readSlot(code, ip) val dst = decoder.readSlot(code, ip)
ip += fn.slotWidth ip += fn.slotWidth
@ -543,6 +547,42 @@ class BytecodeVm {
ip += fn.slotWidth ip += fn.slotWidth
frame.setBool(dst, frame.getReal(a) != frame.getInt(b).toDouble()) 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.NOT_BOOL -> { Opcode.NOT_BOOL -> {
val src = decoder.readSlot(code, ip) val src = decoder.readSlot(code, ip)
ip += fn.slotWidth ip += fn.slotWidth

View File

@ -83,6 +83,10 @@ enum class Opcode(val code: Int) {
CMP_GTE_REAL_INT(0x69), CMP_GTE_REAL_INT(0x69),
CMP_NEQ_INT_REAL(0x6A), CMP_NEQ_INT_REAL(0x6A),
CMP_NEQ_REAL_INT(0x6B), 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), NOT_BOOL(0x70),
AND_BOOL(0x71), AND_BOOL(0x71),

View File

@ -30,6 +30,8 @@ import net.sergeych.lyng.obj.ObjFalse
import net.sergeych.lyng.obj.ObjInt import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjTrue import net.sergeych.lyng.obj.ObjTrue
import net.sergeych.lyng.obj.ObjReal 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.AssignRef
import net.sergeych.lyng.obj.ValueFnRef import net.sergeych.lyng.obj.ValueFnRef
import net.sergeych.lyng.obj.ObjVoid import net.sergeych.lyng.obj.ObjVoid
@ -226,4 +228,47 @@ class BytecodeVmTest {
val result = BytecodeVm().execute(fn, Scope(), emptyList()) val result = BytecodeVm().execute(fn, Scope(), emptyList())
assertEquals(4, result.toInt()) 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())
}
} }