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.
- 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
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
- RET S
- 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
- CALL_DIRECT F, S, C, S

View File

@ -2556,174 +2556,23 @@ class Compiler(
}
val loopSlotPlanSnapshot = slotPlanIndices(loopSlotPlan)
return object : Statement() {
override val pos: Pos = body.pos
override suspend fun execute(scope: Scope): Obj {
val forContext = scope.createChildScope(start)
if (loopSlotPlanSnapshot.isNotEmpty()) {
forContext.applySlotPlan(loopSlotPlanSnapshot)
}
// loop var: StoredObject
val loopSO = forContext.addItem(tVar.value, true, ObjNull)
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
}
}
}
return ForInStatement(
loopVarName = tVar.value,
source = source,
constRange = constRange,
body = body,
elseStatement = elseStatement,
label = label,
canBreak = canBreak,
loopSlotPlan = loopSlotPlanSnapshot,
pos = body.pos
)
} else {
// maybe other loops?
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? {
if (ref !is RangeRef) 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")
private suspend fun parseDoWhileStatement(): Statement {
val label = getLabel()?.also { cc.labels += it }

View File

@ -129,7 +129,7 @@ class BytecodeBuilder {
private fun operandKinds(op: Opcode): List<OperandKind> {
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.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 ->
@ -138,6 +138,8 @@ class BytecodeBuilder {
listOf(OperandKind.SLOT)
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL ->
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_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,

View File

@ -42,6 +42,7 @@ class BytecodeCompiler(
return when (stmt) {
is ExpressionStatement -> compileExpression(name, stmt)
is net.sergeych.lyng.IfStatement -> compileIf(name, stmt)
is net.sergeych.lyng.ForInStatement -> compileForIn(name, stmt)
else -> null
}
}
@ -71,6 +72,10 @@ class BytecodeCompiler(
is BinaryOpRef -> compileBinary(ref)
is UnaryOpRef -> compileUnary(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 MethodCallRef -> compileMethodCall(ref)
else -> null
@ -590,6 +595,173 @@ class BytecodeCompiler(
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 fun compileCall(ref: CallRef): CompiledValue? {
@ -680,6 +852,54 @@ class BytecodeCompiler(
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? {
return when (stmt) {
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) {
when (value.type) {
SlotType.INT -> builder.emit(Opcode.MOVE_INT, value.slot, dstSlot)
@ -764,6 +1039,21 @@ class BytecodeCompiler(
collectScopeSlots(stmt.ifBody)
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 -> {}
}
}
@ -797,6 +1087,20 @@ class BytecodeCompiler(
}
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 -> {
collectScopeSlotsRef(ref.target)
collectScopeSlotsArgs(ref.args)

View File

@ -25,4 +25,5 @@ sealed class BytecodeConst {
data class RealVal(val value: Double) : BytecodeConst()
data class StringVal(val value: String) : 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> {
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.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 ->
@ -91,6 +91,8 @@ object BytecodeDisassembler {
listOf(OperandKind.SLOT)
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL ->
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_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,

View File

@ -22,7 +22,9 @@ import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.*
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)
for (i in args.indices) {
frame.setObj(frame.argBase + i, args[i])
@ -721,6 +723,21 @@ class BytecodeVm {
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 -> {
val calleeSlot = decoder.readSlot(code, ip)
ip += fn.slotWidth

View File

@ -107,6 +107,8 @@ enum class Opcode(val code: Int) {
JMP_IF_FALSE(0x82),
RET(0x83),
RET_VOID(0x84),
PUSH_SCOPE(0x85),
POP_SCOPE(0x86),
CALL_DIRECT(0x90),
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 */
class ConditionalRef(
private val condition: ObjRef,
private val ifTrue: ObjRef,
private val ifFalse: ObjRef
internal val condition: ObjRef,
internal val ifTrue: ObjRef,
internal val ifFalse: ObjRef
) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
return evalCondition(scope).get(scope)
@ -661,9 +661,9 @@ class QualifiedThisMethodSlotCallRef(
/** Assignment compound op: target op= value */
class AssignOpRef(
private val op: BinOp,
private val target: ObjRef,
private val value: ObjRef,
internal val op: BinOp,
internal val target: ObjRef,
internal val value: ObjRef,
private val atPos: Pos,
) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
@ -723,9 +723,9 @@ class AssignOpRef(
/** Pre/post ++/-- on l-values */
class IncDecRef(
private val target: ObjRef,
private val isIncrement: Boolean,
private val isPost: Boolean,
internal val target: ObjRef,
internal val isIncrement: Boolean,
internal val isPost: Boolean,
private val atPos: Pos,
) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
@ -751,7 +751,7 @@ class IncDecRef(
}
/** 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 {
val a = left.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.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.toBool
import net.sergeych.lyng.obj.toInt
import net.sergeych.lyng.obj.toLong
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(
val expr: Statement,
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