Add iterator cancellation for bytecode for-in loops
This commit is contained in:
parent
0e069382a2
commit
e346e7e56e
@ -3401,26 +3401,15 @@ class Compiler(
|
||||
val names = mutableListOf<String>()
|
||||
pattern.forEachVariable { names.add(it) }
|
||||
|
||||
return object : Statement() {
|
||||
override val pos: Pos = start
|
||||
override suspend fun execute(context: Scope): Obj {
|
||||
val value = initialExpression.execute(context)
|
||||
for (name in names) {
|
||||
context.addItem(name, true, ObjVoid, visibility, isTransient = isTransient)
|
||||
}
|
||||
pattern.setAt(start, context, value)
|
||||
if (!isMutable) {
|
||||
for (name in names) {
|
||||
val rec = context.objects[name]!!
|
||||
val immutableRec = rec.copy(isMutable = false)
|
||||
context.objects[name] = immutableRec
|
||||
context.localBindings[name] = immutableRec
|
||||
context.updateSlotFor(name, immutableRec)
|
||||
}
|
||||
}
|
||||
return ObjVoid
|
||||
}
|
||||
}
|
||||
return DestructuringVarDeclStatement(
|
||||
pattern,
|
||||
names,
|
||||
initialExpression,
|
||||
isMutable,
|
||||
visibility,
|
||||
isTransient,
|
||||
start
|
||||
)
|
||||
}
|
||||
|
||||
if (nextToken.type != Token.Type.ID)
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.ListLiteralRef
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
|
||||
class DestructuringVarDeclStatement(
|
||||
val pattern: ListLiteralRef,
|
||||
val names: List<String>,
|
||||
val initializer: Statement,
|
||||
val isMutable: Boolean,
|
||||
val visibility: Visibility,
|
||||
val isTransient: Boolean,
|
||||
override val pos: Pos,
|
||||
) : Statement() {
|
||||
override suspend fun execute(context: Scope): Obj {
|
||||
val value = initializer.execute(context)
|
||||
for (name in names) {
|
||||
context.addItem(name, true, ObjVoid, visibility, isTransient = isTransient)
|
||||
}
|
||||
pattern.setAt(pos, context, value)
|
||||
if (!isMutable) {
|
||||
for (name in names) {
|
||||
val rec = context.objects[name]!!
|
||||
val immutableRec = rec.copy(isMutable = false)
|
||||
context.objects[name] = immutableRec
|
||||
context.localBindings[name] = immutableRec
|
||||
context.updateSlotFor(name, immutableRec)
|
||||
}
|
||||
}
|
||||
return ObjVoid
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@
|
||||
package net.sergeych.lyng.bytecode
|
||||
|
||||
import net.sergeych.lyng.BlockStatement
|
||||
import net.sergeych.lyng.DestructuringVarDeclStatement
|
||||
import net.sergeych.lyng.ExpressionStatement
|
||||
import net.sergeych.lyng.IfStatement
|
||||
import net.sergeych.lyng.ParsedArgument
|
||||
@ -69,6 +70,7 @@ class BytecodeCompiler(
|
||||
val continueLabel: CmdBuilder.Label,
|
||||
val breakFlagSlot: Int,
|
||||
val resultSlot: Int?,
|
||||
val hasIterator: Boolean,
|
||||
)
|
||||
|
||||
fun compileStatement(name: String, stmt: net.sergeych.lyng.Statement): CmdFunction? {
|
||||
@ -1973,6 +1975,7 @@ class BytecodeCompiler(
|
||||
}
|
||||
is BlockStatement -> emitBlock(target, true)
|
||||
is VarDeclStatement -> emitVarDecl(target)
|
||||
is DestructuringVarDeclStatement -> emitStatementEval(target)
|
||||
is net.sergeych.lyng.ExtensionPropertyDeclStatement -> emitExtensionPropertyDecl(target)
|
||||
is net.sergeych.lyng.ClassDeclStatement -> emitStatementEval(target)
|
||||
is net.sergeych.lyng.FunctionDeclStatement -> emitStatementEval(target)
|
||||
@ -2018,6 +2021,7 @@ class BytecodeCompiler(
|
||||
}
|
||||
is BlockStatement -> emitBlock(target, false)
|
||||
is VarDeclStatement -> emitVarDecl(target)
|
||||
is DestructuringVarDeclStatement -> emitStatementEval(target)
|
||||
is net.sergeych.lyng.ExtensionPropertyDeclStatement -> emitExtensionPropertyDecl(target)
|
||||
is net.sergeych.lyng.BreakStatement -> compileBreak(target)
|
||||
is net.sergeych.lyng.ContinueStatement -> compileContinue(target)
|
||||
@ -2202,6 +2206,7 @@ class BytecodeCompiler(
|
||||
val iterSlot = allocSlot()
|
||||
val iteratorId = builder.addConst(BytecodeConst.StringVal("iterator"))
|
||||
builder.emit(Opcode.CALL_VIRTUAL, sourceObj.slot, iteratorId, 0, 0, iterSlot)
|
||||
builder.emit(Opcode.ITER_PUSH, iterSlot)
|
||||
|
||||
val breakFlagSlot = allocSlot()
|
||||
val falseId = builder.addConst(BytecodeConst.Bool(false))
|
||||
@ -2235,7 +2240,14 @@ class BytecodeCompiler(
|
||||
updateSlotTypeByName(stmt.loopVarName, SlotType.OBJ)
|
||||
|
||||
loopStack.addLast(
|
||||
LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null)
|
||||
LoopContext(
|
||||
stmt.label,
|
||||
endLabel,
|
||||
continueLabel,
|
||||
breakFlagSlot,
|
||||
if (wantResult) resultSlot else null,
|
||||
hasIterator = true
|
||||
)
|
||||
)
|
||||
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
|
||||
loopStack.removeLast()
|
||||
@ -2247,6 +2259,13 @@ class BytecodeCompiler(
|
||||
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(loopLabel)))
|
||||
|
||||
builder.mark(endLabel)
|
||||
val afterPop = builder.label()
|
||||
builder.emit(
|
||||
Opcode.JMP_IF_TRUE,
|
||||
listOf(CmdBuilder.Operand.IntVal(breakFlagSlot), CmdBuilder.Operand.LabelRef(afterPop))
|
||||
)
|
||||
builder.emit(Opcode.ITER_POP)
|
||||
builder.mark(afterPop)
|
||||
if (stmt.elseStatement != null) {
|
||||
val afterElse = builder.label()
|
||||
builder.emit(
|
||||
@ -2316,7 +2335,14 @@ class BytecodeCompiler(
|
||||
updateSlotType(loopSlotId, SlotType.INT)
|
||||
updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
|
||||
loopStack.addLast(
|
||||
LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null)
|
||||
LoopContext(
|
||||
stmt.label,
|
||||
endLabel,
|
||||
continueLabel,
|
||||
breakFlagSlot,
|
||||
if (wantResult) resultSlot else null,
|
||||
hasIterator = false
|
||||
)
|
||||
)
|
||||
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
|
||||
loopStack.removeLast()
|
||||
@ -2375,7 +2401,14 @@ class BytecodeCompiler(
|
||||
updateSlotType(loopSlotId, SlotType.INT)
|
||||
updateSlotTypeByName(stmt.loopVarName, SlotType.INT)
|
||||
loopStack.addLast(
|
||||
LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null)
|
||||
LoopContext(
|
||||
stmt.label,
|
||||
endLabel,
|
||||
continueLabel,
|
||||
breakFlagSlot,
|
||||
if (wantResult) resultSlot else null,
|
||||
hasIterator = false
|
||||
)
|
||||
)
|
||||
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
|
||||
loopStack.removeLast()
|
||||
@ -2429,7 +2462,14 @@ class BytecodeCompiler(
|
||||
listOf(CmdBuilder.Operand.IntVal(condition.slot), CmdBuilder.Operand.LabelRef(endLabel))
|
||||
)
|
||||
loopStack.addLast(
|
||||
LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null)
|
||||
LoopContext(
|
||||
stmt.label,
|
||||
endLabel,
|
||||
continueLabel,
|
||||
breakFlagSlot,
|
||||
if (wantResult) resultSlot else null,
|
||||
hasIterator = false
|
||||
)
|
||||
)
|
||||
val bodyValue = compileLoopBody(stmt.body, wantResult) ?: return null
|
||||
loopStack.removeLast()
|
||||
@ -2471,7 +2511,14 @@ class BytecodeCompiler(
|
||||
val endLabel = builder.label()
|
||||
builder.mark(loopLabel)
|
||||
loopStack.addLast(
|
||||
LoopContext(stmt.label, endLabel, continueLabel, breakFlagSlot, if (wantResult) resultSlot else null)
|
||||
LoopContext(
|
||||
stmt.label,
|
||||
endLabel,
|
||||
continueLabel,
|
||||
breakFlagSlot,
|
||||
if (wantResult) resultSlot else null,
|
||||
hasIterator = false
|
||||
)
|
||||
)
|
||||
val bodyValue = compileStatementValueOrFallback(stmt.body, wantResult) ?: return null
|
||||
loopStack.removeLast()
|
||||
@ -2577,17 +2624,28 @@ class BytecodeCompiler(
|
||||
}
|
||||
}
|
||||
|
||||
private fun findLoopContext(label: String?): LoopContext? {
|
||||
private fun findLoopContextIndex(label: String?): Int? {
|
||||
if (loopStack.isEmpty()) return null
|
||||
if (label == null) return loopStack.last()
|
||||
for (ctx in loopStack.reversed()) {
|
||||
if (ctx.label == label) return ctx
|
||||
val stack = loopStack.toList()
|
||||
if (label == null) return stack.lastIndex
|
||||
for (i in stack.indices.reversed()) {
|
||||
if (stack[i].label == label) return i
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun emitIteratorCancel(stack: List<LoopContext>, startIndex: Int) {
|
||||
for (i in stack.lastIndex downTo startIndex) {
|
||||
if (stack[i].hasIterator) {
|
||||
builder.emit(Opcode.ITER_CANCEL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun compileBreak(stmt: net.sergeych.lyng.BreakStatement): CompiledValue? {
|
||||
val ctx = findLoopContext(stmt.label) ?: return null
|
||||
val stack = loopStack.toList()
|
||||
val targetIndex = findLoopContextIndex(stmt.label) ?: return null
|
||||
val ctx = stack[targetIndex]
|
||||
val value = stmt.resultExpr?.let { compileStatementValueOrFallback(it) }
|
||||
if (ctx.resultSlot != null) {
|
||||
val objValue = value?.let { ensureObjSlot(it) } ?: run {
|
||||
@ -2601,6 +2659,7 @@ class BytecodeCompiler(
|
||||
} else if (value != null) {
|
||||
ensureObjSlot(value)
|
||||
}
|
||||
emitIteratorCancel(stack, targetIndex)
|
||||
val trueId = builder.addConst(BytecodeConst.Bool(true))
|
||||
builder.emit(Opcode.CONST_BOOL, trueId, ctx.breakFlagSlot)
|
||||
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(ctx.breakLabel)))
|
||||
@ -2608,7 +2667,12 @@ class BytecodeCompiler(
|
||||
}
|
||||
|
||||
private fun compileContinue(stmt: net.sergeych.lyng.ContinueStatement): CompiledValue? {
|
||||
val ctx = findLoopContext(stmt.label) ?: return null
|
||||
val stack = loopStack.toList()
|
||||
val targetIndex = findLoopContextIndex(stmt.label) ?: return null
|
||||
val ctx = stack[targetIndex]
|
||||
if (targetIndex < stack.lastIndex) {
|
||||
emitIteratorCancel(stack, targetIndex + 1)
|
||||
}
|
||||
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(ctx.continueLabel)))
|
||||
return CompiledValue(ctx.breakFlagSlot, SlotType.BOOL)
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ package net.sergeych.lyng.bytecode
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.Statement
|
||||
import net.sergeych.lyng.DestructuringVarDeclStatement
|
||||
import net.sergeych.lyng.WhenCase
|
||||
import net.sergeych.lyng.WhenCondition
|
||||
import net.sergeych.lyng.WhenEqualsCondition
|
||||
@ -100,6 +101,8 @@ class BytecodeStatement private constructor(
|
||||
target.statements().any { containsUnsupportedStatement(it) }
|
||||
is net.sergeych.lyng.VarDeclStatement ->
|
||||
target.initializer?.let { containsUnsupportedStatement(it) } ?: false
|
||||
is net.sergeych.lyng.DestructuringVarDeclStatement ->
|
||||
containsUnsupportedStatement(target.initializer)
|
||||
is net.sergeych.lyng.BreakStatement ->
|
||||
target.resultExpr?.let { containsUnsupportedStatement(it) } ?: false
|
||||
is net.sergeych.lyng.ContinueStatement -> false
|
||||
@ -147,6 +150,17 @@ class BytecodeStatement private constructor(
|
||||
stmt.pos
|
||||
)
|
||||
}
|
||||
is net.sergeych.lyng.DestructuringVarDeclStatement -> {
|
||||
net.sergeych.lyng.DestructuringVarDeclStatement(
|
||||
stmt.pattern,
|
||||
stmt.names,
|
||||
unwrapDeep(stmt.initializer),
|
||||
stmt.isMutable,
|
||||
stmt.visibility,
|
||||
stmt.isTransient,
|
||||
stmt.pos
|
||||
)
|
||||
}
|
||||
is net.sergeych.lyng.IfStatement -> {
|
||||
net.sergeych.lyng.IfStatement(
|
||||
unwrapDeep(stmt.condition),
|
||||
|
||||
@ -192,6 +192,10 @@ class CmdBuilder {
|
||||
listOf(OperandKind.ID, OperandKind.SLOT)
|
||||
Opcode.EVAL_FALLBACK, Opcode.EVAL_REF, Opcode.EVAL_STMT, Opcode.EVAL_VALUE_FN ->
|
||||
listOf(OperandKind.ID, OperandKind.SLOT)
|
||||
Opcode.ITER_PUSH ->
|
||||
listOf(OperandKind.SLOT)
|
||||
Opcode.ITER_POP, Opcode.ITER_CANCEL ->
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@ -387,6 +391,9 @@ class CmdBuilder {
|
||||
Opcode.EVAL_REF -> CmdEvalRef(operands[0], operands[1])
|
||||
Opcode.EVAL_STMT -> CmdEvalStmt(operands[0], operands[1])
|
||||
Opcode.EVAL_VALUE_FN -> CmdEvalValueFn(operands[0], operands[1])
|
||||
Opcode.ITER_PUSH -> CmdIterPush(operands[0])
|
||||
Opcode.ITER_POP -> CmdIterPop()
|
||||
Opcode.ITER_CANCEL -> CmdIterCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,6 +191,9 @@ object CmdDisassembler {
|
||||
is CmdEvalRef -> Opcode.EVAL_REF to intArrayOf(cmd.id, cmd.dst)
|
||||
is CmdEvalStmt -> Opcode.EVAL_STMT to intArrayOf(cmd.id, cmd.dst)
|
||||
is CmdEvalValueFn -> Opcode.EVAL_VALUE_FN to intArrayOf(cmd.id, cmd.dst)
|
||||
is CmdIterPush -> Opcode.ITER_PUSH to intArrayOf(cmd.iterSlot)
|
||||
is CmdIterPop -> Opcode.ITER_POP to intArrayOf()
|
||||
is CmdIterCancel -> Opcode.ITER_CANCEL to intArrayOf()
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,7 +208,8 @@ object CmdDisassembler {
|
||||
|
||||
private fun operandKinds(op: Opcode): List<OperandKind> {
|
||||
return when (op) {
|
||||
Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE, Opcode.POP_SLOT_PLAN -> emptyList()
|
||||
Opcode.NOP, Opcode.RET_VOID, Opcode.POP_SCOPE, Opcode.POP_SLOT_PLAN,
|
||||
Opcode.ITER_POP, Opcode.ITER_CANCEL -> 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 ->
|
||||
@ -248,7 +252,7 @@ object CmdDisassembler {
|
||||
Opcode.ADD_OBJ, Opcode.SUB_OBJ, Opcode.MUL_OBJ, Opcode.DIV_OBJ, Opcode.MOD_OBJ, Opcode.CONTAINS_OBJ,
|
||||
Opcode.AND_BOOL, Opcode.OR_BOOL ->
|
||||
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
|
||||
Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET ->
|
||||
Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET, Opcode.ITER_PUSH ->
|
||||
listOf(OperandKind.SLOT)
|
||||
Opcode.JMP ->
|
||||
listOf(OperandKind.IP)
|
||||
|
||||
@ -32,11 +32,17 @@ class CmdVm {
|
||||
result = null
|
||||
val frame = CmdFrame(this, fn, scope0, args)
|
||||
val cmds = fn.cmds
|
||||
while (result == null) {
|
||||
val cmd = cmds[frame.ip]
|
||||
frame.ip += 1
|
||||
cmd.perform(frame)
|
||||
try {
|
||||
while (result == null) {
|
||||
val cmd = cmds[frame.ip]
|
||||
frame.ip += 1
|
||||
cmd.perform(frame)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
frame.cancelIterators()
|
||||
throw e
|
||||
}
|
||||
frame.cancelIterators()
|
||||
return result ?: ObjVoid
|
||||
}
|
||||
}
|
||||
@ -1419,6 +1425,27 @@ class CmdEvalValueFn(internal val id: Int, internal val dst: Int) : Cmd() {
|
||||
}
|
||||
}
|
||||
|
||||
class CmdIterPush(internal val iterSlot: Int) : Cmd() {
|
||||
override suspend fun perform(frame: CmdFrame) {
|
||||
frame.pushIterator(frame.slotToObj(iterSlot))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
class CmdIterPop : Cmd() {
|
||||
override suspend fun perform(frame: CmdFrame) {
|
||||
frame.popIterator()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
class CmdIterCancel : Cmd() {
|
||||
override suspend fun perform(frame: CmdFrame) {
|
||||
frame.cancelTopIterator()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
class CmdFrame(
|
||||
val vm: CmdVm,
|
||||
val fn: CmdFunction,
|
||||
@ -1440,6 +1467,7 @@ class CmdFrame(
|
||||
internal val slotPlanScopeStack = ArrayDeque<Boolean>()
|
||||
private var scopeDepth = 0
|
||||
private var virtualDepth = 0
|
||||
private val iterStack = ArrayDeque<Obj>()
|
||||
|
||||
internal val frame = BytecodeFrame(fn.localCount, args.size)
|
||||
private val addrScopes: Array<Scope?> = arrayOfNulls(fn.addrCount)
|
||||
@ -1484,6 +1512,26 @@ class CmdFrame(
|
||||
scopeDepth -= 1
|
||||
}
|
||||
|
||||
fun pushIterator(iter: Obj) {
|
||||
iterStack.addLast(iter)
|
||||
}
|
||||
|
||||
fun popIterator() {
|
||||
iterStack.removeLastOrNull()
|
||||
}
|
||||
|
||||
suspend fun cancelTopIterator() {
|
||||
val iter = iterStack.removeLastOrNull() ?: return
|
||||
iter.invokeInstanceMethod(scope, "cancelIteration") { ObjVoid }
|
||||
}
|
||||
|
||||
suspend fun cancelIterators() {
|
||||
while (iterStack.isNotEmpty()) {
|
||||
val iter = iterStack.removeLast()
|
||||
iter.invokeInstanceMethod(scope, "cancelIteration") { ObjVoid }
|
||||
}
|
||||
}
|
||||
|
||||
fun pushSlotPlan(plan: Map<String, Int>) {
|
||||
if (scope.hasSlotPlanConflict(plan)) {
|
||||
scopeStack.addLast(scope)
|
||||
|
||||
@ -149,6 +149,9 @@ enum class Opcode(val code: Int) {
|
||||
EVAL_REF(0xBC),
|
||||
EVAL_STMT(0xBD),
|
||||
EVAL_VALUE_FN(0xBE),
|
||||
ITER_PUSH(0xBF),
|
||||
ITER_POP(0xC0),
|
||||
ITER_CANCEL(0xC1),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
@ -186,7 +186,6 @@ class ScriptTest {
|
||||
assertEquals(0, ti.cancelCount)
|
||||
}
|
||||
|
||||
@Ignore("Bytecode: unsupported or incorrect behavior")
|
||||
@Test
|
||||
fun testForLoopCancelsOnBreak() = runTest {
|
||||
val scope = Script.newScope()
|
||||
@ -202,7 +201,6 @@ class ScriptTest {
|
||||
assertEquals(1, ti.cancelCount)
|
||||
}
|
||||
|
||||
@Ignore("Bytecode: unsupported or incorrect behavior")
|
||||
@Test
|
||||
fun testForLoopCancelsOnException() = runTest {
|
||||
val scope = Script.newScope()
|
||||
@ -4567,7 +4565,6 @@ class ScriptTest {
|
||||
}
|
||||
|
||||
|
||||
@Ignore("Bytecode: unsupported or incorrect behavior")
|
||||
@Test
|
||||
fun testDestructuringAssignment() = runTest {
|
||||
eval(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user