Bytecode for iterable for-in loops

This commit is contained in:
Sergey Chernov 2026-01-28 08:23:04 +03:00
parent 37a8831fd7
commit 8dfdbaa0a0
8 changed files with 185 additions and 15 deletions

View File

@ -422,8 +422,7 @@ class Compiler(
(target.elseBody?.let { containsUnsupportedForBytecode(it) } ?: false)
}
is ForInStatement -> {
target.constRange == null ||
containsUnsupportedForBytecode(target.source) ||
containsUnsupportedForBytecode(target.source) ||
containsUnsupportedForBytecode(target.body) ||
(target.elseStatement?.let { containsUnsupportedForBytecode(it) } ?: false)
}
@ -2079,8 +2078,8 @@ class Compiler(
cc.currentPos(),
"when else block already defined"
)
elseCase =
parseStatement() ?: throw ScriptError(
elseCase = parseStatement()?.let { unwrapBytecodeDeep(it) }
?: throw ScriptError(
cc.currentPos(),
"when else block expected"
)
@ -2102,7 +2101,8 @@ class Compiler(
}
// parsed conditions?
if (!skipParseBody) {
val block = parseStatement() ?: throw ScriptError(cc.currentPos(), "when case block expected")
val block = parseStatement()?.let { unwrapBytecodeDeep(it) }
?: throw ScriptError(cc.currentPos(), "when case block expected")
for (c in currentCondition) cases += WhenCase(c, block)
}
}

View File

@ -1679,10 +1679,81 @@ class BytecodeCompiler(
rangeRef = extractRangeFromLocal(stmt.source)
}
val typedRangeLocal = if (range == null && rangeRef == null) extractTypedRangeLocal(stmt.source) else null
if (range == null && rangeRef == null && typedRangeLocal == null) return null
val loopLocalIndex = localSlotIndexByName[stmt.loopVarName] ?: return null
val loopSlotId = scopeSlotCount + loopLocalIndex
if (range == null && rangeRef == null && typedRangeLocal == null) {
val sourceValue = compileStatementValueOrFallback(stmt.source) ?: return null
val sourceObj = ensureObjSlot(sourceValue)
val typeId = builder.addConst(BytecodeConst.ObjRef(ObjIterable))
val typeSlot = allocSlot()
builder.emit(Opcode.CONST_OBJ, typeId, typeSlot)
builder.emit(Opcode.ASSERT_IS, sourceObj.slot, typeSlot)
val iterSlot = allocSlot()
val iteratorId = builder.addConst(BytecodeConst.StringVal("iterator"))
builder.emit(Opcode.CALL_VIRTUAL, sourceObj.slot, iteratorId, 0, 0, iterSlot)
val breakFlagSlot = allocSlot()
val falseId = builder.addConst(BytecodeConst.Bool(false))
builder.emit(Opcode.CONST_BOOL, falseId, breakFlagSlot)
val resultSlot = allocSlot()
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
builder.emit(Opcode.CONST_OBJ, voidId, resultSlot)
val loopLabel = builder.label()
val continueLabel = builder.label()
val endLabel = builder.label()
builder.mark(loopLabel)
val hasNextSlot = allocSlot()
val hasNextId = builder.addConst(BytecodeConst.StringVal("hasNext"))
builder.emit(Opcode.CALL_VIRTUAL, iterSlot, hasNextId, 0, 0, hasNextSlot)
val condSlot = allocSlot()
builder.emit(Opcode.OBJ_TO_BOOL, hasNextSlot, condSlot)
builder.emit(
Opcode.JMP_IF_FALSE,
listOf(CmdBuilder.Operand.IntVal(condSlot), CmdBuilder.Operand.LabelRef(endLabel))
)
val nextSlot = allocSlot()
val nextId = builder.addConst(BytecodeConst.StringVal("next"))
builder.emit(Opcode.CALL_VIRTUAL, iterSlot, nextId, 0, 0, nextSlot)
val nextObj = ensureObjSlot(CompiledValue(nextSlot, SlotType.UNKNOWN))
builder.emit(Opcode.MOVE_OBJ, nextObj.slot, loopSlotId)
updateSlotType(loopSlotId, SlotType.OBJ)
updateSlotTypeByName(stmt.loopVarName, SlotType.OBJ)
loopStack.addLast(
LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null)
)
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
loopStack.removeLast()
if (wantResult) {
val bodyObj = ensureObjSlot(bodyValue)
builder.emit(Opcode.MOVE_OBJ, bodyObj.slot, resultSlot)
}
builder.mark(continueLabel)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel)))
builder.mark(endLabel)
if (stmt.elseStatement != null) {
val afterElse = builder.label()
builder.emit(
Opcode.JMP_IF_TRUE,
listOf(CmdBuilder.Operand.IntVal(breakFlagSlot), CmdBuilder.Operand.LabelRef(afterElse))
)
val elseValue = compileStatementValueOrFallback(stmt.elseStatement, wantResult) ?: return null
if (wantResult) {
val elseObj = ensureObjSlot(elseValue)
builder.emit(Opcode.MOVE_OBJ, elseObj.slot, resultSlot)
}
builder.mark(afterElse)
}
return resultSlot
}
val iSlot = allocSlot()
val endSlot = allocSlot()
if (range != null) {

View File

@ -81,14 +81,7 @@ class BytecodeStatement private constructor(
(target.elseBody?.let { containsUnsupportedStatement(it) } ?: false)
}
is net.sergeych.lyng.ForInStatement -> {
val rangeSource = target.source
val rangeRef = (rangeSource as? net.sergeych.lyng.ExpressionStatement)?.ref as? RangeRef
val sourceRef = (rangeSource as? net.sergeych.lyng.ExpressionStatement)?.ref
val hasRange = target.constRange != null ||
rangeRef != null ||
(sourceRef is net.sergeych.lyng.obj.LocalSlotRef)
val unsupported = !hasRange ||
containsUnsupportedStatement(target.source) ||
val unsupported = containsUnsupportedStatement(target.source) ||
containsUnsupportedStatement(target.body) ||
(target.elseStatement?.let { containsUnsupportedStatement(it) } ?: false)
unsupported

View File

@ -120,8 +120,12 @@ class CmdBuilder {
Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE, Opcode.POP_SLOT_PLAN -> 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 ->
Opcode.OBJ_TO_BOOL,
Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT,
Opcode.ASSERT_IS ->
listOf(OperandKind.SLOT, OperandKind.SLOT)
Opcode.CHECK_IS ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RANGE_INT_BOUNDS ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RET_LABEL, Opcode.THROW ->
@ -211,7 +215,10 @@ class CmdBuilder {
Opcode.CONST_BOOL -> CmdConstBool(operands[0], operands[1])
Opcode.CONST_NULL -> CmdConstNull(operands[0])
Opcode.BOX_OBJ -> CmdBoxObj(operands[0], operands[1])
Opcode.OBJ_TO_BOOL -> CmdObjToBool(operands[0], operands[1])
Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3])
Opcode.CHECK_IS -> CmdCheckIs(operands[0], operands[1], operands[2])
Opcode.ASSERT_IS -> CmdAssertIs(operands[0], operands[1])
Opcode.RET_LABEL -> CmdRetLabel(operands[0], operands[1])
Opcode.THROW -> CmdThrow(operands[0], operands[1])
Opcode.RESOLVE_SCOPE_SLOT -> CmdResolveScopeSlot(operands[0], operands[1])

View File

@ -68,6 +68,9 @@ object CmdDisassembler {
is CmdConstBool -> Opcode.CONST_BOOL to intArrayOf(cmd.constId, cmd.dst)
is CmdConstNull -> Opcode.CONST_NULL to intArrayOf(cmd.dst)
is CmdBoxObj -> Opcode.BOX_OBJ to intArrayOf(cmd.src, cmd.dst)
is CmdObjToBool -> Opcode.OBJ_TO_BOOL to intArrayOf(cmd.src, cmd.dst)
is CmdCheckIs -> Opcode.CHECK_IS to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst)
is CmdAssertIs -> Opcode.ASSERT_IS to intArrayOf(cmd.objSlot, cmd.typeSlot)
is CmdRangeIntBounds -> Opcode.RANGE_INT_BOUNDS to intArrayOf(cmd.src, cmd.startSlot, cmd.endSlot, cmd.okSlot)
is CmdResolveScopeSlot -> Opcode.RESOLVE_SCOPE_SLOT to intArrayOf(cmd.scopeSlot, cmd.addrSlot)
is CmdLoadObjAddr -> Opcode.LOAD_OBJ_ADDR to intArrayOf(cmd.addrSlot, cmd.dst)
@ -197,6 +200,10 @@ object CmdDisassembler {
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.OBJ_TO_BOOL, Opcode.ASSERT_IS ->
listOf(OperandKind.SLOT, OperandKind.SLOT)
Opcode.CHECK_IS ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RANGE_INT_BOUNDS ->
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
Opcode.RET_LABEL, Opcode.THROW ->

View File

@ -154,6 +154,37 @@ class CmdBoxObj(internal val src: Int, internal val dst: Int) : Cmd() {
}
}
class CmdObjToBool(internal val src: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
frame.setBool(dst, frame.slotToObj(src).toBool())
return
}
}
class CmdCheckIs(internal val objSlot: Int, internal val typeSlot: Int, internal val dst: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val obj = frame.slotToObj(objSlot)
val typeObj = frame.slotToObj(typeSlot)
val clazz = typeObj as? ObjClass
frame.setBool(dst, clazz != null && obj.isInstanceOf(clazz))
return
}
}
class CmdAssertIs(internal val objSlot: Int, internal val typeSlot: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val obj = frame.slotToObj(objSlot)
val typeObj = frame.slotToObj(typeSlot)
val clazz = typeObj as? ObjClass ?: frame.scope.raiseClassCastError(
"${typeObj.inspect(frame.scope)} is not the class instance"
)
if (!obj.isInstanceOf(clazz)) {
frame.scope.raiseClassCastError("expected ${clazz.className}, got ${obj.objClass.className}")
}
return
}
}
class CmdRangeIntBounds(
internal val src: Int,
internal val startSlot: Int,

View File

@ -34,6 +34,9 @@ enum class Opcode(val code: Int) {
REAL_TO_INT(0x11),
BOOL_TO_INT(0x12),
INT_TO_BOOL(0x13),
OBJ_TO_BOOL(0x14),
CHECK_IS(0x15),
ASSERT_IS(0x16),
ADD_INT(0x20),
SUB_INT(0x21),

View File

@ -5044,4 +5044,62 @@ class ScriptTest {
assertEquals( [1], t(1) )
""".trimIndent())
}
@Test
fun testForInIterableDisasm() = runTest {
val scope = Script.newScope()
scope.eval("""
fun type(x) {
when(x) {
"42", 42 -> "answer to the great question"
is Real, is Int -> "number"
is String -> {
for( d in x ) {
if( d !in '0'..'9' )
break "unknown"
}
else "number"
}
}
}
""".trimIndent())
println("[DEBUG_LOG] type disasm:\n${scope.disassembleSymbol("type")}")
val r1 = scope.eval("""type("12%")""")
val r2 = scope.eval("""type("153")""")
println("[DEBUG_LOG] type(\"12%\")=${r1.inspect(scope)}")
println("[DEBUG_LOG] type(\"153\")=${r2.inspect(scope)}")
}
@Test
fun testForInIterableBytecode() = runTest {
val result = eval("""
fun sumAll(x) {
var s = 0
for (i in x) s += i
s
}
sumAll([1,2,3]) + sumAll(0..3)
""".trimIndent())
assertEquals(ObjInt(12), result)
}
@Test
fun testForInIterableUnknownTypeDisasm() = runTest {
val scope = Script.newScope()
scope.eval("""
fun countAll(x) {
var c = 0
for (i in x) c++
c
}
""".trimIndent())
val disasm = scope.disassembleSymbol("countAll")
println("[DEBUG_LOG] countAll disasm:\n$disasm")
assertFalse(disasm.contains("not a compiled body"))
assertFalse(disasm.contains("EVAL_FALLBACK"))
val r1 = scope.eval("countAll([1,2,3])")
val r2 = scope.eval("countAll(0..3)")
assertEquals(ObjInt(3), r1)
assertEquals(ObjInt(4), r2)
}
}