Expand bytecode expressions and loops

This commit is contained in:
Sergey Chernov 2026-01-26 05:47:37 +03:00
parent 72901d9d4c
commit 144082733c
11 changed files with 590 additions and 209 deletions

View File

@ -30,6 +30,9 @@ slots[localCount .. localCount+argCount-1] arguments
- scopeSlotNames: array sized scopeSlotCount, each entry nullable. - scopeSlotNames: array sized scopeSlotCount, each entry nullable.
- Intended for disassembly/debug tooling; VM semantics do not depend on it. - Intended for disassembly/debug tooling; VM semantics do not depend on it.
### Constant pool extras
- SlotPlan: map of name -> slot index, used by PUSH_SCOPE to pre-allocate and map loop locals.
## 2) Slot ID Width ## 2) Slot ID Width
Per frame, select: Per frame, select:
@ -185,6 +188,12 @@ Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass.
- JMP_IF_FALSE S, I - JMP_IF_FALSE S, I
- RET S - RET S
- RET_VOID - RET_VOID
- PUSH_SCOPE K
- POP_SCOPE
### Scope setup
- PUSH_SCOPE uses const `SlotPlan` (name -> slot index) to create a child scope and apply slot mapping.
- POP_SCOPE restores the parent scope.
### Calls ### Calls
- CALL_DIRECT F, S, C, S - CALL_DIRECT F, S, C, S

View File

@ -2556,174 +2556,23 @@ class Compiler(
} }
val loopSlotPlanSnapshot = slotPlanIndices(loopSlotPlan) val loopSlotPlanSnapshot = slotPlanIndices(loopSlotPlan)
return object : Statement() { return ForInStatement(
override val pos: Pos = body.pos loopVarName = tVar.value,
override suspend fun execute(scope: Scope): Obj { source = source,
val forContext = scope.createChildScope(start) constRange = constRange,
if (loopSlotPlanSnapshot.isNotEmpty()) { body = body,
forContext.applySlotPlan(loopSlotPlanSnapshot) elseStatement = elseStatement,
} label = label,
canBreak = canBreak,
// loop var: StoredObject loopSlotPlan = loopSlotPlanSnapshot,
val loopSO = forContext.addItem(tVar.value, true, ObjNull) pos = body.pos
if (constRange != null && PerfFlags.PRIMITIVE_FASTOPS) {
val loopSlotIndex = forContext.getSlotIndexOf(tVar.value) ?: -1
return loopIntRange(
forContext,
constRange.start,
constRange.endExclusive,
loopSO,
loopSlotIndex,
body,
elseStatement,
label,
canBreak
) )
}
// insofar we suggest source object is enumerable. Later we might need to add checks
val sourceObj = source.execute(forContext)
if (sourceObj is ObjRange && sourceObj.isIntRange && PerfFlags.PRIMITIVE_FASTOPS) {
val loopSlotIndex = forContext.getSlotIndexOf(tVar.value) ?: -1
return loopIntRange(
forContext,
sourceObj.start!!.toLong(),
if (sourceObj.isEndInclusive)
sourceObj.end!!.toLong() + 1
else
sourceObj.end!!.toLong(),
loopSO,
loopSlotIndex,
body,
elseStatement,
label,
canBreak
)
} else if (sourceObj.isInstanceOf(ObjIterable)) {
return loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak)
} else {
val size = runCatching { sourceObj.readField(forContext, "size").value.toInt() }
.getOrElse {
throw ScriptError(
tOp.pos,
"object is not enumerable: no size in $sourceObj",
it
)
}
var result: Obj = ObjVoid
var breakCaught = false
if (size > 0) {
var current = runCatching { sourceObj.getAt(forContext, ObjInt.of(0)) }
.getOrElse {
throw ScriptError(
tOp.pos,
"object is not enumerable: no index access for ${sourceObj.inspect(scope)}",
it
)
}
var index = 0
while (true) {
loopSO.value = current
try {
result = body.execute(forContext)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
breakCaught = true
if (lbe.doContinue) continue
else {
result = lbe.result
break
}
} else
throw lbe
}
if (++index >= size) break
current = sourceObj.getAt(forContext, ObjInt.of(index.toLong()))
}
}
if (!breakCaught && elseStatement != null) {
result = elseStatement.execute(scope)
}
return result
}
}
}
} else { } else {
// maybe other loops? // maybe other loops?
throw ScriptError(tOp.pos, "Unsupported for-loop syntax") throw ScriptError(tOp.pos, "Unsupported for-loop syntax")
} }
} }
private suspend fun loopIntRange(
forScope: Scope, start: Long, end: Long, loopVar: ObjRecord, loopSlotIndex: Int,
body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean
): Obj {
var result: Obj = ObjVoid
val cacheLow = ObjInt.CACHE_LOW
val cacheHigh = ObjInt.CACHE_HIGH
val useCache = start >= cacheLow && end <= cacheHigh + 1
val cache = if (useCache) ObjInt.cacheArray() else null
val useSlot = loopSlotIndex >= 0
if (catchBreak) {
if (useCache && cache != null) {
var i = start
while (i < end) {
val v = cache[(i - cacheLow).toInt()]
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
try {
result = body.execute(forScope)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) {
i++
continue
}
return lbe.result
}
throw lbe
}
i++
}
} else {
for (i in start..<end) {
val v = ObjInt.of(i)
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
try {
result = body.execute(forScope)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
return lbe.result
}
throw lbe
}
}
}
} else {
if (useCache && cache != null) {
var i = start
while (i < end) {
val v = cache[(i - cacheLow).toInt()]
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
result = body.execute(forScope)
i++
}
} else {
for (i in start..<end) {
val v = ObjInt.of(i)
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
result = body.execute(forScope)
}
}
}
return elseStatement?.execute(forScope) ?: result
}
private data class ConstIntRange(val start: Long, val endExclusive: Long)
private fun constIntRangeOrNull(ref: ObjRef): ConstIntRange? { private fun constIntRangeOrNull(ref: ObjRef): ConstIntRange? {
if (ref !is RangeRef) return null if (ref !is RangeRef) return null
val start = constIntValueOrNull(ref.left) ?: return null val start = constIntValueOrNull(ref.left) ?: return null
@ -2743,40 +2592,6 @@ class Compiler(
} }
} }
private suspend fun loopIterable(
forScope: Scope, sourceObj: Obj, loopVar: ObjRecord,
body: Statement, elseStatement: Statement?, label: String?,
catchBreak: Boolean
): Obj {
var result: Obj = ObjVoid
var breakCaught = false
sourceObj.enumerate(forScope) { item ->
loopVar.value = item
if (catchBreak) {
try {
result = body.execute(forScope)
true
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) true
else {
result = lbe.result
breakCaught = true
false
}
} else
throw lbe
}
} else {
result = body.execute(forScope)
true
}
}
return if (!breakCaught && elseStatement != null) {
elseStatement.execute(forScope)
} else result
}
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
private suspend fun parseDoWhileStatement(): Statement { private suspend fun parseDoWhileStatement(): Statement {
val label = getLabel()?.also { cc.labels += it } val label = getLabel()?.also { cc.labels += it }

View File

@ -129,7 +129,7 @@ class BytecodeBuilder {
private fun operandKinds(op: Opcode): List<OperandKind> { private fun operandKinds(op: Opcode): List<OperandKind> {
return when (op) { return when (op) {
Opcode.NOP, Opcode.RET_VOID -> emptyList() Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE -> emptyList()
Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ, Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ,
Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_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 -> Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT ->
@ -138,6 +138,8 @@ class BytecodeBuilder {
listOf(OperandKind.SLOT) listOf(OperandKind.SLOT)
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL -> Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.PUSH_SCOPE ->
listOf(OperandKind.CONST)
Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT, 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.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.AND_INT, Opcode.OR_INT, Opcode.XOR_INT, Opcode.SHL_INT, Opcode.SHR_INT, Opcode.USHR_INT,

View File

@ -42,6 +42,7 @@ class BytecodeCompiler(
return when (stmt) { return when (stmt) {
is ExpressionStatement -> compileExpression(name, stmt) is ExpressionStatement -> compileExpression(name, stmt)
is net.sergeych.lyng.IfStatement -> compileIf(name, stmt) is net.sergeych.lyng.IfStatement -> compileIf(name, stmt)
is net.sergeych.lyng.ForInStatement -> compileForIn(name, stmt)
else -> null else -> null
} }
} }
@ -71,6 +72,10 @@ class BytecodeCompiler(
is BinaryOpRef -> compileBinary(ref) is BinaryOpRef -> compileBinary(ref)
is UnaryOpRef -> compileUnary(ref) is UnaryOpRef -> compileUnary(ref)
is AssignRef -> compileAssign(ref) is AssignRef -> compileAssign(ref)
is AssignOpRef -> compileAssignOp(ref)
is IncDecRef -> compileIncDec(ref)
is ConditionalRef -> compileConditional(ref)
is ElvisRef -> compileElvis(ref)
is CallRef -> compileCall(ref) is CallRef -> compileCall(ref)
is MethodCallRef -> compileMethodCall(ref) is MethodCallRef -> compileMethodCall(ref)
else -> null else -> null
@ -590,6 +595,173 @@ class BytecodeCompiler(
return CompiledValue(slot, value.type) return CompiledValue(slot, value.type)
} }
private fun compileAssignOp(ref: AssignOpRef): CompiledValue? {
val target = ref.target as? LocalSlotRef ?: return null
if (!allowLocalSlots) return null
if (!target.isMutable || target.isDelegated) return null
if (refDepth(target) > 0) return null
val slot = scopeSlotMap[ScopeSlotKey(refDepth(target), refSlot(target))] ?: return null
val targetType = slotTypes[slot] ?: return null
val rhs = compileRef(ref.value) ?: return null
val out = slot
val result = when (ref.op) {
BinOp.PLUS -> compileAssignOpBinary(targetType, rhs, out, Opcode.ADD_INT, Opcode.ADD_REAL, Opcode.ADD_OBJ)
BinOp.MINUS -> compileAssignOpBinary(targetType, rhs, out, Opcode.SUB_INT, Opcode.SUB_REAL, Opcode.SUB_OBJ)
BinOp.STAR -> compileAssignOpBinary(targetType, rhs, out, Opcode.MUL_INT, Opcode.MUL_REAL, Opcode.MUL_OBJ)
BinOp.SLASH -> compileAssignOpBinary(targetType, rhs, out, Opcode.DIV_INT, Opcode.DIV_REAL, Opcode.DIV_OBJ)
BinOp.PERCENT -> compileAssignOpBinary(targetType, rhs, out, Opcode.MOD_INT, null, Opcode.MOD_OBJ)
else -> null
} ?: return null
updateSlotType(out, result.type)
return CompiledValue(out, result.type)
}
private fun compileAssignOpBinary(
targetType: SlotType,
rhs: CompiledValue,
out: Int,
intOp: Opcode,
realOp: Opcode?,
objOp: Opcode?,
): CompiledValue? {
return when (targetType) {
SlotType.INT -> {
when (rhs.type) {
SlotType.INT -> {
builder.emit(intOp, out, rhs.slot, out)
CompiledValue(out, SlotType.INT)
}
SlotType.REAL -> {
if (realOp == null) return null
val left = allocSlot()
builder.emit(Opcode.INT_TO_REAL, out, left)
builder.emit(realOp, left, rhs.slot, out)
CompiledValue(out, SlotType.REAL)
}
else -> null
}
}
SlotType.REAL -> {
if (realOp == null) return null
when (rhs.type) {
SlotType.REAL -> {
builder.emit(realOp, out, rhs.slot, out)
CompiledValue(out, SlotType.REAL)
}
SlotType.INT -> {
val right = allocSlot()
builder.emit(Opcode.INT_TO_REAL, rhs.slot, right)
builder.emit(realOp, out, right, out)
CompiledValue(out, SlotType.REAL)
}
else -> null
}
}
SlotType.OBJ -> {
if (objOp == null) return null
if (rhs.type != SlotType.OBJ) return null
builder.emit(objOp, out, rhs.slot, out)
CompiledValue(out, SlotType.OBJ)
}
else -> null
}
}
private fun compileIncDec(ref: IncDecRef): CompiledValue? {
val target = ref.target as? LocalSlotRef ?: return null
if (!allowLocalSlots) return null
if (!target.isMutable || target.isDelegated) return null
if (refDepth(target) > 0) return null
val slot = scopeSlotMap[ScopeSlotKey(refDepth(target), refSlot(target))] ?: return null
val slotType = slotTypes[slot] ?: return null
return when (slotType) {
SlotType.INT -> {
if (ref.isPost) {
val old = allocSlot()
builder.emit(Opcode.MOVE_INT, slot, old)
builder.emit(if (ref.isIncrement) Opcode.INC_INT else Opcode.DEC_INT, slot)
CompiledValue(old, SlotType.INT)
} else {
builder.emit(if (ref.isIncrement) Opcode.INC_INT else Opcode.DEC_INT, slot)
CompiledValue(slot, SlotType.INT)
}
}
SlotType.REAL -> {
val oneSlot = allocSlot()
val oneId = builder.addConst(BytecodeConst.RealVal(1.0))
builder.emit(Opcode.CONST_REAL, oneId, oneSlot)
if (ref.isPost) {
val old = allocSlot()
builder.emit(Opcode.MOVE_REAL, slot, old)
val op = if (ref.isIncrement) Opcode.ADD_REAL else Opcode.SUB_REAL
builder.emit(op, slot, oneSlot, slot)
CompiledValue(old, SlotType.REAL)
} else {
val op = if (ref.isIncrement) Opcode.ADD_REAL else Opcode.SUB_REAL
builder.emit(op, slot, oneSlot, slot)
CompiledValue(slot, SlotType.REAL)
}
}
else -> null
}
}
private fun compileConditional(ref: ConditionalRef): CompiledValue? {
val condition = compileRefWithFallback(ref.condition, SlotType.BOOL, Pos.builtIn) ?: return null
if (condition.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(condition.slot), BytecodeBuilder.Operand.LabelRef(elseLabel))
)
val thenValue = compileRefWithFallback(ref.ifTrue, null, Pos.builtIn) ?: return null
val thenObj = ensureObjSlot(thenValue)
builder.emit(Opcode.MOVE_OBJ, thenObj.slot, resultSlot)
builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel)))
builder.mark(elseLabel)
val elseValue = compileRefWithFallback(ref.ifFalse, null, Pos.builtIn) ?: return null
val elseObj = ensureObjSlot(elseValue)
builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot)
builder.mark(endLabel)
updateSlotType(resultSlot, SlotType.OBJ)
return CompiledValue(resultSlot, SlotType.OBJ)
}
private fun compileElvis(ref: ElvisRef): CompiledValue? {
val leftValue = compileRefWithFallback(ref.left, null, Pos.builtIn) ?: return null
val leftObj = ensureObjSlot(leftValue)
val resultSlot = allocSlot()
val nullSlot = allocSlot()
builder.emit(Opcode.CONST_NULL, nullSlot)
val cmpSlot = allocSlot()
builder.emit(Opcode.CMP_REF_EQ_OBJ, leftObj.slot, nullSlot, cmpSlot)
val rightLabel = builder.label()
val endLabel = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(BytecodeBuilder.Operand.IntVal(cmpSlot), BytecodeBuilder.Operand.LabelRef(rightLabel))
)
builder.emit(Opcode.MOVE_OBJ, leftObj.slot, resultSlot)
builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel)))
builder.mark(rightLabel)
val rightValue = compileRefWithFallback(ref.right, null, Pos.builtIn) ?: return null
val rightObj = ensureObjSlot(rightValue)
builder.emit(Opcode.MOVE_OBJ, rightObj.slot, resultSlot)
builder.mark(endLabel)
updateSlotType(resultSlot, SlotType.OBJ)
return CompiledValue(resultSlot, SlotType.OBJ)
}
private fun ensureObjSlot(value: CompiledValue): CompiledValue {
if (value.type == SlotType.OBJ) return value
val dst = allocSlot()
builder.emit(Opcode.BOX_OBJ, value.slot, dst)
updateSlotType(dst, SlotType.OBJ)
return CompiledValue(dst, SlotType.OBJ)
}
private data class CallArgs(val base: Int, val count: Int) private data class CallArgs(val base: Int, val count: Int)
private fun compileCall(ref: CallRef): CompiledValue? { private fun compileCall(ref: CallRef): CompiledValue? {
@ -680,6 +852,54 @@ class BytecodeCompiler(
return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames) return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames)
} }
private fun compileForIn(name: String, stmt: net.sergeych.lyng.ForInStatement): BytecodeFunction? {
if (stmt.canBreak) return null
val range = stmt.constRange ?: return null
val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName] ?: return null
val loopSlot = scopeSlotMap[ScopeSlotKey(0, loopSlotIndex)] ?: return null
val planId = builder.addConst(BytecodeConst.SlotPlan(stmt.loopSlotPlan))
builder.emit(Opcode.PUSH_SCOPE, planId)
val iSlot = allocSlot()
val endSlot = allocSlot()
val startId = builder.addConst(BytecodeConst.IntVal(range.start))
val endId = builder.addConst(BytecodeConst.IntVal(range.endExclusive))
builder.emit(Opcode.CONST_INT, startId, iSlot)
builder.emit(Opcode.CONST_INT, endId, endSlot)
val resultSlot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
builder.emit(Opcode.CONST_OBJ, voidId, resultSlot)
val loopLabel = builder.label()
val endLabel = builder.label()
builder.mark(loopLabel)
val cmpSlot = allocSlot()
builder.emit(Opcode.CMP_GTE_INT, iSlot, endSlot, cmpSlot)
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(BytecodeBuilder.Operand.IntVal(cmpSlot), BytecodeBuilder.Operand.LabelRef(endLabel))
)
builder.emit(Opcode.MOVE_INT, iSlot, loopSlot)
val bodyValue = compileStatementValueOrFallback(stmt.body) ?: return null
val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot)
builder.emit(Opcode.INC_INT, iSlot)
builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(loopLabel)))
builder.mark(endLabel)
if (stmt.elseStatement != null) {
val elseValue = compileStatementValueOrFallback(stmt.elseStatement) ?: return null
val elseObj = ensureObjSlot(elseValue)
builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot)
}
builder.emit(Opcode.POP_SCOPE)
builder.emit(Opcode.RET, resultSlot)
val localCount = maxOf(nextSlot, resultSlot + 1) - scopeSlotCount
return builder.build(name, localCount, scopeSlotDepths, scopeSlotIndices, scopeSlotNames)
}
private fun compileStatementValue(stmt: Statement): CompiledValue? { private fun compileStatementValue(stmt: Statement): CompiledValue? {
return when (stmt) { return when (stmt) {
is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos) is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos)
@ -687,6 +907,61 @@ class BytecodeCompiler(
} }
} }
private fun compileStatementValueOrFallback(stmt: Statement): CompiledValue? {
return when (stmt) {
is ExpressionStatement -> compileRefWithFallback(stmt.ref, null, stmt.pos)
is IfStatement -> compileIfExpression(stmt)
else -> {
val slot = allocSlot()
val id = builder.addFallback(stmt)
builder.emit(Opcode.EVAL_FALLBACK, id, slot)
updateSlotType(slot, SlotType.OBJ)
CompiledValue(slot, SlotType.OBJ)
}
}
}
private fun compileIfExpression(stmt: IfStatement): CompiledValue? {
val condition = compileCondition(stmt.condition, stmt.pos) ?: return null
if (condition.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(condition.slot), BytecodeBuilder.Operand.LabelRef(elseLabel))
)
val thenValue = compileStatementValueOrFallback(stmt.ifBody) ?: return null
val thenObj = ensureObjSlot(thenValue)
builder.emit(Opcode.MOVE_OBJ, thenObj.slot, resultSlot)
builder.emit(Opcode.JMP, listOf(BytecodeBuilder.Operand.LabelRef(endLabel)))
builder.mark(elseLabel)
if (stmt.elseBody != null) {
val elseValue = compileStatementValueOrFallback(stmt.elseBody) ?: return null
val elseObj = ensureObjSlot(elseValue)
builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot)
} else {
val id = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
builder.emit(Opcode.CONST_OBJ, id, resultSlot)
}
builder.mark(endLabel)
updateSlotType(resultSlot, SlotType.OBJ)
return CompiledValue(resultSlot, SlotType.OBJ)
}
private fun compileCondition(stmt: Statement, pos: Pos): CompiledValue? {
return when (stmt) {
is ExpressionStatement -> compileRefWithFallback(stmt.ref, SlotType.BOOL, stmt.pos)
else -> {
val slot = allocSlot()
val id = builder.addFallback(ToBoolStatement(stmt, pos))
builder.emit(Opcode.EVAL_FALLBACK, id, slot)
updateSlotType(slot, SlotType.BOOL)
CompiledValue(slot, SlotType.BOOL)
}
}
}
private fun emitMove(value: CompiledValue, dstSlot: Int) { private fun emitMove(value: CompiledValue, dstSlot: Int) {
when (value.type) { when (value.type) {
SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, dstSlot) SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, dstSlot)
@ -764,6 +1039,21 @@ class BytecodeCompiler(
collectScopeSlots(stmt.ifBody) collectScopeSlots(stmt.ifBody)
stmt.elseBody?.let { collectScopeSlots(it) } stmt.elseBody?.let { collectScopeSlots(it) }
} }
is net.sergeych.lyng.ForInStatement -> {
val loopSlotIndex = stmt.loopSlotPlan[stmt.loopVarName]
if (loopSlotIndex != null) {
val key = ScopeSlotKey(0, loopSlotIndex)
if (!scopeSlotMap.containsKey(key)) {
scopeSlotMap[key] = scopeSlotMap.size
}
if (!scopeSlotNameMap.containsKey(key)) {
scopeSlotNameMap[key] = stmt.loopVarName
}
}
collectScopeSlots(stmt.source)
collectScopeSlots(stmt.body)
stmt.elseStatement?.let { collectScopeSlots(it) }
}
else -> {} else -> {}
} }
} }
@ -797,6 +1087,20 @@ class BytecodeCompiler(
} }
collectScopeSlotsRef(assignValue(ref)) collectScopeSlotsRef(assignValue(ref))
} }
is AssignOpRef -> {
collectScopeSlotsRef(ref.target)
collectScopeSlotsRef(ref.value)
}
is IncDecRef -> collectScopeSlotsRef(ref.target)
is ConditionalRef -> {
collectScopeSlotsRef(ref.condition)
collectScopeSlotsRef(ref.ifTrue)
collectScopeSlotsRef(ref.ifFalse)
}
is ElvisRef -> {
collectScopeSlotsRef(ref.left)
collectScopeSlotsRef(ref.right)
}
is CallRef -> { is CallRef -> {
collectScopeSlotsRef(ref.target) collectScopeSlotsRef(ref.target)
collectScopeSlotsArgs(ref.args) collectScopeSlotsArgs(ref.args)

View File

@ -25,4 +25,5 @@ sealed class BytecodeConst {
data class RealVal(val value: Double) : BytecodeConst() data class RealVal(val value: Double) : BytecodeConst()
data class StringVal(val value: String) : BytecodeConst() data class StringVal(val value: String) : BytecodeConst()
data class ObjRef(val value: Obj) : BytecodeConst() data class ObjRef(val value: Obj) : BytecodeConst()
data class SlotPlan(val plan: Map<String, Int>) : BytecodeConst()
} }

View File

@ -82,7 +82,7 @@ object BytecodeDisassembler {
private fun operandKinds(op: Opcode): List<OperandKind> { private fun operandKinds(op: Opcode): List<OperandKind> {
return when (op) { return when (op) {
Opcode.NOP, Opcode.RET_VOID -> emptyList() Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE -> emptyList()
Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ, Opcode.MOVE_OBJ, Opcode.MOVE_INT, Opcode.MOVE_REAL, Opcode.MOVE_BOOL, Opcode.BOX_OBJ,
Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_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 -> Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT ->
@ -91,6 +91,8 @@ object BytecodeDisassembler {
listOf(OperandKind.SLOT) listOf(OperandKind.SLOT)
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL -> Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL ->
listOf(OperandKind.CONST, OperandKind.SLOT) listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.PUSH_SCOPE ->
listOf(OperandKind.CONST)
Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT, 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.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.AND_INT, Opcode.OR_INT, Opcode.XOR_INT, Opcode.SHL_INT, Opcode.SHR_INT, Opcode.USHR_INT,

View File

@ -22,7 +22,9 @@ import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
class BytecodeVm { class BytecodeVm {
suspend fun execute(fn: BytecodeFunction, scope: Scope, args: List<Obj>): Obj { suspend fun execute(fn: BytecodeFunction, scope0: Scope, args: List<Obj>): Obj {
val scopeStack = ArrayDeque<Scope>()
var scope = scope0
val frame = BytecodeFrame(fn.localCount, args.size) val frame = BytecodeFrame(fn.localCount, args.size)
for (i in args.indices) { for (i in args.indices) {
frame.setObj(frame.argBase + i, args[i]) frame.setObj(frame.argBase + i, args[i])
@ -721,6 +723,21 @@ class BytecodeVm {
ip = target ip = target
} }
} }
Opcode.PUSH_SCOPE -> {
val constId = decoder.readConstId(code, ip, fn.constIdWidth)
ip += fn.constIdWidth
val planConst = fn.constants[constId] as? BytecodeConst.SlotPlan
?: error("PUSH_SCOPE expects SlotPlan at $constId")
scopeStack.addLast(scope)
scope = scope.createChildScope()
if (planConst.plan.isNotEmpty()) {
scope.applySlotPlan(planConst.plan)
}
}
Opcode.POP_SCOPE -> {
scope = scopeStack.removeLastOrNull()
?: error("Scope stack underflow in POP_SCOPE")
}
Opcode.CALL_SLOT -> { Opcode.CALL_SLOT -> {
val calleeSlot = decoder.readSlot(code, ip) val calleeSlot = decoder.readSlot(code, ip)
ip += fn.slotWidth ip += fn.slotWidth

View File

@ -107,6 +107,8 @@ enum class Opcode(val code: Int) {
JMP_IF_FALSE(0x82), JMP_IF_FALSE(0x82),
RET(0x83), RET(0x83),
RET_VOID(0x84), RET_VOID(0x84),
PUSH_SCOPE(0x85),
POP_SCOPE(0x86),
CALL_DIRECT(0x90), CALL_DIRECT(0x90),
CALL_VIRTUAL(0x91), CALL_VIRTUAL(0x91),

View File

@ -385,9 +385,9 @@ class BinaryOpRef(internal val op: BinOp, internal val left: ObjRef, internal va
/** Conditional (ternary) operator reference: cond ? a : b */ /** Conditional (ternary) operator reference: cond ? a : b */
class ConditionalRef( class ConditionalRef(
private val condition: ObjRef, internal val condition: ObjRef,
private val ifTrue: ObjRef, internal val ifTrue: ObjRef,
private val ifFalse: ObjRef internal val ifFalse: ObjRef
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
return evalCondition(scope).get(scope) return evalCondition(scope).get(scope)
@ -661,9 +661,9 @@ class QualifiedThisMethodSlotCallRef(
/** Assignment compound op: target op= value */ /** Assignment compound op: target op= value */
class AssignOpRef( class AssignOpRef(
private val op: BinOp, internal val op: BinOp,
private val target: ObjRef, internal val target: ObjRef,
private val value: ObjRef, internal val value: ObjRef,
private val atPos: Pos, private val atPos: Pos,
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
@ -723,9 +723,9 @@ class AssignOpRef(
/** Pre/post ++/-- on l-values */ /** Pre/post ++/-- on l-values */
class IncDecRef( class IncDecRef(
private val target: ObjRef, internal val target: ObjRef,
private val isIncrement: Boolean, internal val isIncrement: Boolean,
private val isPost: Boolean, internal val isPost: Boolean,
private val atPos: Pos, private val atPos: Pos,
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
@ -751,7 +751,7 @@ class IncDecRef(
} }
/** Elvis operator reference: a ?: b */ /** Elvis operator reference: a ?: b */
class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { class ElvisRef(internal val left: ObjRef, internal val right: ObjRef) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val a = left.evalValue(scope) val a = left.evalValue(scope)
val r = if (a != ObjNull) a else right.evalValue(scope) val r = if (a != ObjNull) a else right.evalValue(scope)

View File

@ -19,8 +19,15 @@ package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjIterable
import net.sergeych.lyng.obj.ObjNull
import net.sergeych.lyng.obj.ObjRange
import net.sergeych.lyng.obj.ObjRecord
import net.sergeych.lyng.obj.ObjVoid import net.sergeych.lyng.obj.ObjVoid
import net.sergeych.lyng.obj.toBool import net.sergeych.lyng.obj.toBool
import net.sergeych.lyng.obj.toInt
import net.sergeych.lyng.obj.toLong
fun String.toSource(name: String = "eval"): Source = Source(name, this) fun String.toSource(name: String = "eval"): Source = Source(name, this)
@ -79,6 +86,217 @@ class IfStatement(
} }
} }
data class ConstIntRange(val start: Long, val endExclusive: Long)
class ForInStatement(
val loopVarName: String,
val source: Statement,
val constRange: ConstIntRange?,
val body: Statement,
val elseStatement: Statement?,
val label: String?,
val canBreak: Boolean,
val loopSlotPlan: Map<String, Int>,
override val pos: Pos,
) : Statement() {
override suspend fun execute(scope: Scope): Obj {
val forContext = scope.createChildScope(pos)
if (loopSlotPlan.isNotEmpty()) {
forContext.applySlotPlan(loopSlotPlan)
}
val loopSO = forContext.addItem(loopVarName, true, ObjNull)
val loopSlotIndex = forContext.getSlotIndexOf(loopVarName) ?: -1
if (constRange != null && PerfFlags.PRIMITIVE_FASTOPS) {
return loopIntRange(
forContext,
constRange.start,
constRange.endExclusive,
loopSO,
loopSlotIndex,
body,
elseStatement,
label,
canBreak
)
}
val sourceObj = source.execute(forContext)
return if (sourceObj is ObjRange && sourceObj.isIntRange && PerfFlags.PRIMITIVE_FASTOPS) {
loopIntRange(
forContext,
sourceObj.start!!.toLong(),
if (sourceObj.isEndInclusive) sourceObj.end!!.toLong() + 1 else sourceObj.end!!.toLong(),
loopSO,
loopSlotIndex,
body,
elseStatement,
label,
canBreak
)
} else if (sourceObj.isInstanceOf(ObjIterable)) {
loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak)
} else {
val size = runCatching { sourceObj.readField(forContext, "size").value.toInt() }
.getOrElse {
throw ScriptError(
pos,
"object is not enumerable: no size in $sourceObj",
it
)
}
var result: Obj = ObjVoid
var breakCaught = false
if (size > 0) {
var current = runCatching { sourceObj.getAt(forContext, ObjInt.of(0)) }
.getOrElse {
throw ScriptError(
pos,
"object is not enumerable: no index access for ${sourceObj.inspect(scope)}",
it
)
}
var index = 0
while (true) {
loopSO.value = current
try {
result = body.execute(forContext)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
breakCaught = true
if (lbe.doContinue) continue
result = lbe.result
break
} else {
throw lbe
}
}
if (++index >= size) break
current = sourceObj.getAt(forContext, ObjInt.of(index.toLong()))
}
}
if (!breakCaught && elseStatement != null) {
result = elseStatement.execute(scope)
}
result
}
}
private suspend fun loopIntRange(
forScope: Scope,
start: Long,
end: Long,
loopVar: ObjRecord,
loopSlotIndex: Int,
body: Statement,
elseStatement: Statement?,
label: String?,
catchBreak: Boolean,
): Obj {
var result: Obj = ObjVoid
val cacheLow = ObjInt.CACHE_LOW
val cacheHigh = ObjInt.CACHE_HIGH
val useCache = start >= cacheLow && end <= cacheHigh + 1
val cache = if (useCache) ObjInt.cacheArray() else null
val useSlot = loopSlotIndex >= 0
if (catchBreak) {
if (useCache && cache != null) {
var i = start
while (i < end) {
val v = cache[(i - cacheLow).toInt()]
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
try {
result = body.execute(forScope)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) {
i++
continue
}
return lbe.result
}
throw lbe
}
i++
}
} else {
for (i in start..<end) {
val v = ObjInt.of(i)
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
try {
result = body.execute(forScope)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
return lbe.result
}
throw lbe
}
}
}
} else {
if (useCache && cache != null) {
var i = start
while (i < end) {
val v = cache[(i - cacheLow).toInt()]
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
result = body.execute(forScope)
i++
}
} else {
for (i in start..<end) {
val v = ObjInt.of(i)
if (useSlot) forScope.setSlotValue(loopSlotIndex, v) else loopVar.value = v
result = body.execute(forScope)
}
}
}
return elseStatement?.execute(forScope) ?: result
}
private suspend fun loopIterable(
forScope: Scope,
sourceObj: Obj,
loopVar: ObjRecord,
body: Statement,
elseStatement: Statement?,
label: String?,
catchBreak: Boolean,
): Obj {
var result: Obj = ObjVoid
var breakCaught = false
sourceObj.enumerate(forScope) { item ->
loopVar.value = item
if (catchBreak) {
try {
result = body.execute(forScope)
true
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
breakCaught = true
if (lbe.doContinue) true else {
result = lbe.result
false
}
} else {
throw lbe
}
}
} else {
result = body.execute(forScope)
true
}
}
if (!breakCaught && elseStatement != null) {
result = elseStatement.execute(forScope)
}
return result
}
}
class ToBoolStatement( class ToBoolStatement(
val expr: Statement, val expr: Statement,
override val pos: Pos, override val pos: Pos,

View File

@ -0,0 +1,11 @@
# Bytecode expression + for-in loop support
Changes
- Added bytecode compilation for conditional/elvis expressions, inc/dec, and compound assignments where safe.
- Added ForInStatement and ConstIntRange to keep for-loop structure explicit (no anonymous Statement).
- Added PUSH_SCOPE/POP_SCOPE opcodes with SlotPlan constants to create loop scopes in bytecode.
- Bytecode compiler emits int-range for-in loops when const range is known and no break/continue.
Tests
- ./gradlew :lynglib:jvmTest
- ./gradlew :lynglib:allTests -x :lynglib:jvmTest