diff --git a/codeGenIntermediate/src/prog8/codegen/intermediate/StackFrameLayout.kt b/codeGenIntermediate/src/prog8/codegen/intermediate/StackFrameLayout.kt index 7606b3dc5..bead3600e 100644 --- a/codeGenIntermediate/src/prog8/codegen/intermediate/StackFrameLayout.kt +++ b/codeGenIntermediate/src/prog8/codegen/intermediate/StackFrameLayout.kt @@ -83,7 +83,7 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro val indirectCallers = allSubs.filter { sub -> containsIndirectCall(sub) }.map { it.label }.toSet() val reentrant = subsWithMultipleLiveActivations(callSites, indirectCallers, dispatchTargets) val framePointerClobberers = framePointerClobberingSubs() - val reachesFramePointerClobber = subsReachingFramePointerClobber(framePointerClobberers) + val reachesFramePointerClobber = subsReachingFramePointerClobber(framePointerClobberers, indirectCallers) val framedSubs = mutableMapOf() val notFramedReasons = mutableMapOf() @@ -102,7 +102,7 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro rewriteOutgoingJumps(framedSubs.keys) } - private class CallSiteRef(val chunk: IRCodeChunkBase, val index: Int, val caller: String, val site: CallSite) + private class CallSiteRef(val caller: String, val site: CallSite) /** all direct calls between normal subroutines, grouped by callee label */ private fun collectCallSites(subLabels: Set): Map> { @@ -114,7 +114,7 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro val site = instrs[i].callSite ?: continue val ref = site.codeReference if (ref is CodeReference.Label && ref.name in subLabels) { - result.getOrPut(ref.name) { mutableListOf() } += CallSiteRef(chunk, i, sub.label, site) + result.getOrPut(ref.name) { mutableListOf() } += CallSiteRef(sub.label, site) } } } @@ -176,12 +176,16 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro return result } + /** a frame-pointer-clobbering subroutine reached from another subroutine, and how it was reached */ + private class ClobberReach(val clobberer: String, val viaIndirect: Boolean) + /** - * Subroutine label -> label of the frame-pointer-clobbering subroutine it can reach, for every - * subroutine that can (directly or indirectly) call one. An indirect call may dispatch to any - * subroutine, so a subroutine containing one is assumed to reach all of them. + * Subroutine label -> the frame-pointer-clobbering subroutine it can reach, for every + * subroutine that can reach one. Only real call edges are followed; a subroutine is only + * credited with reaching a clobberer through an indirect call (viaIndirect=true) when it + * can reach a subroutine that makes one, because such a call may dispatch to anything. */ - private fun subsReachingFramePointerClobber(clobberers: Set): Map { + private fun subsReachingFramePointerClobber(clobberers: Set, indirectCallers: Set): Map { if (clobberers.isEmpty()) return emptyMap() val callTargets = mutableMapOf>() @@ -191,13 +195,12 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro chunk.instructions.forEach { instr -> when (val ref = instr.callSite?.codeReference) { is CodeReference.Label -> targets += ref.name - is CodeReference.Indirect -> targets += clobberers else -> {} } } } } - val reaching = mutableMapOf() + val reaching = mutableMapOf() var changed = true while (changed) { changed = false @@ -205,10 +208,29 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro if (label in reaching) continue val hit = targets.firstOrNull { it in clobberers || it in reaching } ?: continue - reaching[label] = if (hit in clobberers) hit else reaching.getValue(hit) + reaching[label] = if (hit in clobberers) ClobberReach(hit, false) else reaching.getValue(hit) changed = true } } + // no real call chain reaches a clobberer: if an indirect call is reachable (here or + // downstream), it may dispatch to any address-taken subroutine including a clobberer + val reachesIndirect = mutableSetOf() + changed = true + while (changed) { + changed = false + for ((label, targets) in callTargets) { + if (label in reachesIndirect) + continue + if (label in indirectCallers || targets.any { it in reachesIndirect }) { + reachesIndirect += label + changed = true + } + } + } + for (label in callTargets.keys) { + if (label !in reaching && label in reachesIndirect) + reaching[label] = ClobberReach(clobberers.first(), true) + } return reaching } @@ -355,13 +377,9 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro } private class FrameLayoutResult( - val frameSize: Int, - val incomingSize: Int, - /** parameter variable name -> frame offset of its incoming slot */ + //. parameter variable name -> frame offset of its incoming slot val paramSlots: Map, - /** parameter variable name -> right-justification displacement within its slot */ - val paramDisplacements: Map, - /** parameter variable names that keep a static cell and get an entry-time copy-in (§6.1) */ + // parameter variable names that keep a static cell and get an entry-time copy-in (§6.1) val copyInParams: Set ) @@ -380,19 +398,23 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro dispatchTargets: Set, indirectCallers: Set, reentrant: Set, - reachesFramePointerClobber: Map + reachesFramePointerClobber: Map ): Frameability { val static = { reason: String -> Frameability.NotFrameable(reason) } if (sub.hasFrame) return static("it already has a stack frame") - reachesFramePointerClobber[sub.label]?.let { - return static("it calls '$it', whose assembly code overwrites the frame pointer register A5") - } var hasInlineAsm = false sub.forEachChunk { chunk -> if (chunk is IRInlineAsmChunk) hasInlineAsm = true } if (hasInlineAsm) return static("it contains inline assembly") if (sub.label in indirectCallers) return static("it makes an indirect call") if (sub.label in dispatchTargets) return static("its address is taken") if (isLabelReferencedInAsm(sub.label, asmTexts)) return static("it is referenced from inline assembly") + reachesFramePointerClobber[sub.label]?.let { + val how = if (it.viaIndirect) + "an indirect call in its call tree is conservatively assumed to reach any subroutine, including '${it.clobberer}'" + else + "it can reach '${it.clobberer}' through calls" + return static("$how, whose assembly code overwrites the frame pointer register A5") + } // every parameter must be able to move: otherwise the caller keeps writing static cells // IRParam.name is the scoped variable name; call sites refer to the same parameter by its @@ -644,7 +666,7 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro sub.frameSize = frameSize sub.incomingSize = paramSlots.size * PARAM_SLOT_SIZE sub.frameVregSlots = vregSlots - return Frameability.Frameable(FrameLayoutResult(frameSize, sub.incomingSize, paramSlots, paramDisplacements, copyInParams)) + return Frameability.Frameable(FrameLayoutResult(paramSlots, copyInParams)) } /** @@ -847,14 +869,15 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro val sharedVregs = if (framed) emptyList() else vregsUsedIn(sub).keys.toList() if (sharedVars.isEmpty() && sharedVregs.isEmpty() && sharedCopyIns.isEmpty()) continue val reason = when { - !framed -> notFramedReasons[sub.label] ?: "it could not be given a stack frame" + !framed -> "it cannot be given a stack frame (${notFramedReasons[sub.label] ?: "unknown reason"})" sharedCopyIns.isNotEmpty() -> "some parameters must be copied into a static cell for external references" else -> "some of the values it uses cannot be moved into a stack frame" } val details = mutableListOf() for (varName in sharedVars) { val v = program.st.lookup(varName) as? IRStStaticVariable - val why = v?.let { frameableVariableProblem(it, varName, addressTaken, asmTexts) } ?: "it is shared with other code" + val why = v?.let { frameableVariableProblem(it, varName, addressTaken, asmTexts) } + ?: if (framed) "it is shared with other code" else "the subroutine has no stack frame to hold it" details += "variable '${varName.substringAfterLast('.').removePrefix("p8v_")}' ($why)" } for (paramName in sharedCopyIns) { @@ -864,7 +887,7 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro details += "${sharedVregs.size} intermediate value(s)" errors.err("recursive subroutine '${userName(sub)}' cannot be compiled: $reason, because " + "two live activations of it would share the same static storage: ${details.joinToString(", ")}. " + - "Restructure the subroutine so that all of its state can live in a stack frame", + "Restructure the subroutine so it can be given a stack frame holding all of its state", sub.position) } } @@ -1006,7 +1029,7 @@ class StackFrameLayout(private val program: IRProgram, private val errors: IErro for ((callee, layout) in framedSubs) { val params = calleeParameters(callee) ?: continue if (params.isEmpty()) continue - offsetsByCallee[callee] = params.mapIndexed { index, param -> + offsetsByCallee[callee] = params.mapIndexed { _, param -> val scoped = if (param.startsWith("$callee.")) param else "$callee.$param" layout.paramSlots[scoped] ?: 0 } diff --git a/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt b/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt index 1e888451a..464871840 100644 --- a/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt +++ b/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt @@ -290,6 +290,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT // area); the return translation emits `unlk a5` before each rts when this is set. internal var frameActive = false + // the code chunks of the subroutine currently being emitted, to tell a jump that stays + // inside it apart from one that leaves it + private var currentSubChunks: Set = emptySet() + // Virtual registers of the subroutine being emitted, by register number -> frame offset. // A re-entrant subroutine (one that can have two live activations) keeps them per-activation in // its stack frame; every other subroutine uses the flat program-static register file. @@ -303,6 +307,16 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT emitLine("unlk a5") } + /** + * true when a jump to this label lands outside the subroutine currently being emitted. + * An unresolvable label counts as inside: without the chunk map we cannot tell, and guessing + * 'outside' would tear down frames of jumps that stay in the subroutine. + */ + internal fun labelIsOutsideCurrentSub(labelName: String): Boolean { + val target = program.resolveCodeTarget(CodeReference.Label(labelName)) ?: return false + return target !in currentSubChunks + } + // === virtual register file layout (1, 2 or 4 bytes per slot depending on type, word-aligned) === private data class RegFileLayout(val offsets: Map, val totalSize: Int) @@ -767,6 +781,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT emitLine("bsr run_global_inits") frameActive = sub.hasFrame frameVregSlots = sub.frameVregSlots + currentSubChunks = mutableSetOf().also { set -> sub.forEachChunk { set.add(it) } } if (sub.hasFrame) { emitRaw("; stack frame: ${sub.frameSize} bytes of locals, ${sub.incomingSize} bytes of arguments") emitLine(if (sub.frameSize > 0) "link a5,#-${sub.frameSize}" else "link a5,#0") @@ -805,6 +820,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT emitRaw("") frameActive = false frameVregSlots = emptyMap() + currentSubChunks = emptySet() } private fun emitAsmSubroutine(sub: IRAsmSubroutine) { diff --git a/codeGenM68k/src/prog8/codegen/m68k/InstrControl.kt b/codeGenM68k/src/prog8/codegen/m68k/InstrControl.kt index 8925532f1..f9ec64140 100644 --- a/codeGenM68k/src/prog8/codegen/m68k/InstrControl.kt +++ b/codeGenM68k/src/prog8/codegen/m68k/InstrControl.kt @@ -26,7 +26,13 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall Opcode.JUMP -> { invalidateD0Cache() when (val t = insn.requireTarget()) { - is CodeReference.Label -> emitLine("bra ${fixNameSymbols(t.name)}") // PC-relative branch; vasm picks the optimal size and falls back to jmp if out of range + is CodeReference.Label -> { + // a jump to a label in another subroutine ends this activation, so our frame has to be gone + // before we leave: whatever runs next returns to our caller, not to us + if (frameActive && labelIsOutsideCurrentSub(t.name)) + emitFrameUnlk() + emitLine("bra ${fixNameSymbols(t.name)}") // PC-relative branch; vasm picks the optimal size and falls back to jmp if out of range + } is CodeReference.Absolute -> emitLine("jmp ${t.address.value.toHex()}") is CodeReference.Indirect -> error("JUMP needs target") } @@ -36,6 +42,8 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall val ref = insn.requireTarget() as? CodeReference.Indirect ?: error("JUMPI needs an indirect target register") val reg = ref.pointer.intNumber invalidateD0Cache() + // a dispatch jump leaves this subroutine for good: the target's rts must find our caller's return address, so our frame has to be gone first + emitFrameUnlk() if(program.options.compTarget.cpu >= CpuType.M68020) { // 68020+ supports memory-indirect addressing: fetch the target address from memory directly. emitLine("jmp ([${regAddr(reg)}])") diff --git a/codeGenM68k/test/TestStackFrameEmission.kt b/codeGenM68k/test/TestStackFrameEmission.kt index 56dbce23a..7e97123b5 100644 --- a/codeGenM68k/test/TestStackFrameEmission.kt +++ b/codeGenM68k/test/TestStackFrameEmission.kt @@ -58,6 +58,37 @@ class TestStackFrameEmission : FunSpec({ fun generateAsm(frameSize: Int, instructions: List): List = generateAsmWithIncoming(frameSize, instructions, 0, "test.start") + // builds a subroutine with a chunk per extra chunk label, for jump-target scope tests + fun sub(label: String, frameSize: Int, instructions: List, extraChunks: List = emptyList()): IRSubroutine { + val first = IRCodeChunk(label, null) + first.instructions.addAll(instructions) + val sub = IRSubroutine(label, emptyList(), emptyList(), Position.DUMMY, frameSize, 0, emptyMap()) + sub.chunks.add(first) + sub.chunks.addAll(extraChunks) + return sub + } + + fun generateAsmForSubs(vararg subs: IRSubroutine): List { + val options = CompilationOptions.builder(Qemu68kTarget()) + .output(OutputType.RAW) + .zeropage(ZeropageType.DONTUSE) + .floats(false) + .compilerVersion("test") + .memtopAddress(0xffffu) + .optimize(true) + .build() + val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder) + program.options.outputDir = tempRoot + val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY) + block.children.addAll(subs) + program.blocks.add(block) + program.linkChunks() + tempRoot.toFile().deleteRecursively() + tempRoot.toFile().mkdirs() + AsmGen(program, Qemu68kTarget(), null).generate() + return tempRoot.resolve("test.asm").readText().lines().map { it.trim() } + } + test("frameless subroutine emits no frame setup") { val lines = generateAsm(0, listOf( IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.direct("some.var")), @@ -307,4 +338,65 @@ class TestStackFrameEmission : FunSpec({ lines.any { it == "clr.b -36(a5)" } shouldBe true } + test("dispatch jump in a framed subroutine pops the frame before jumping") { + // an on..goto leaves the subroutine for good, so the target's rts must find our caller's + // return address: leaving our frame behind would make it pop garbage + val lines = generateAsm(16, listOf( + IRInstructions.jumpIndirect(5), + IRInstructions.simple(Opcode.RETURN) + )) + val unlkIdx = lines.indexOf("unlk a5") + unlkIdx shouldBe lines.indexOfFirst { it.startsWith("jmp") } - 1 + } + + test("dispatch jump in a frameless subroutine emits no frame teardown") { + val lines = generateAsm(0, listOf( + IRInstructions.jumpIndirect(5), + IRInstructions.simple(Opcode.RETURN) + )) + lines.none { it.startsWith("unlk") } shouldBe true + } + + test("jump to a label in another subroutine pops the frame before jumping") { + // jumping out of the subroutine is a documented way to end it, so our frame must not be + // left behind for whatever runs next (and its eventual rts) to trip over + val lines = generateAsmForSubs( + sub("test.start", frameSize = 16, instructions = listOf( + IRInstructions.jump(CodeReference.Label("test.outside")), + IRInstructions.simple(Opcode.RETURN) + )), + sub("test.outside", frameSize = 0, instructions = listOf(IRInstructions.simple(Opcode.RETURN))) + ) + val braIdx = lines.indexOfFirst { it.startsWith("bra") && "test.outside" in it } + (braIdx > 0) shouldBe true + lines[braIdx - 1] shouldBe "unlk a5" + } + + test("jump to a label in a chunk of the same subroutine keeps the frame") { + val lines = generateAsmForSubs( + sub("test.start", frameSize = 16, instructions = listOf( + IRInstructions.jump(CodeReference.Label("test.start.second")), + IRInstructions.simple(Opcode.RETURN) + ), extraChunks = listOf( + IRCodeChunk("test.start.second", null).also { + it.instructions.add(IRInstructions.storeImmediate(IRDataType.BYTE, 0, IRMemory.direct("test.var"))) + } + )) + ) + lines.count { it == "unlk a5" } shouldBe 1 // only the return epilogue + lines[lines.indexOf("unlk a5") + 1] shouldBe "rts" + lines.any { it.startsWith("bra") && "test.start.second" in it } shouldBe true + } + + test("jump to a label inside a loop body of the same subroutine keeps the frame") { + val loopBodyChunk = IRCodeChunk("test.start.loopbody", null) + loopBodyChunk.instructions.add(IRInstructions.jump(CodeReference.Label("test.start.loopbody"))) + val lines = generateAsmForSubs( + sub("test.start", frameSize = 16, instructions = listOf( + IRInstructions.simple(Opcode.RETURN) + ), extraChunks = listOf(IRLoopChunk("test.start.loop", 3, mutableListOf(loopBodyChunk)))) + ) + lines.count { it == "unlk a5" } shouldBe 1 // the jump to the loop body label must not tear the frame down + } + }) diff --git a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt index 8cdca3dae..7ca512a51 100644 --- a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt +++ b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt @@ -1561,6 +1561,8 @@ internal class AstChecker(private val program: Program, errors.undefined(listOf(name), position = arg.position) else if (target !is Subroutine) errors.err("jmptable entry can only be a subroutine: $name", arg.position) + else if (target.parameters.isNotEmpty()) + errors.err("can't dispatch to a subroutine that takes parameters: ${target.name}", arg.position) } } else -> throw SyntaxError("invalid directive ${directive.directive}", directive.position) diff --git a/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt b/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt index 3d41b2c26..63a1494c1 100644 --- a/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt +++ b/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt @@ -888,6 +888,13 @@ _after: if(!indexDt.isUnsignedByte) return noModifications + // a dispatch table has no call site, so nothing would ever pass the target's arguments + ongoto.labels.forEach { labelRef -> + val dispatchTarget = labelRef.targetStatement(program.builtinFunctions) + if(dispatchTarget is Subroutine && dispatchTarget.parameters.isNotEmpty()) + errors.err("can't dispatch to a subroutine that takes parameters: ${dispatchTarget.name}", labelRef.position) + } + val numlabels = ongoto.labels.size val elementDt = program.target.pointerBaseType val arrayDt = DataType.arrayFor(elementDt, program.target) diff --git a/compiler/test/TestStackFrameLocals.kt b/compiler/test/TestStackFrameLocals.kt index ccc3b784d..9d11b8852 100644 --- a/compiler/test/TestStackFrameLocals.kt +++ b/compiler/test/TestStackFrameLocals.kt @@ -6,6 +6,7 @@ import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain import prog8.code.target.C64Target import prog8.code.target.Qemu68kTarget import prog8.intermediate.IRFileReader @@ -1188,4 +1189,37 @@ main { start.any { it.contains(",-(sp)") && it.endsWith("; third") } shouldBe true start.count { it == "subq.l #4,sp" } shouldBe 2 } + + test("re-entrant dispatch subroutines explain why they cannot be framed") { + val src = """ +main { + sub start() { + spawn(&task1) + } + sub spawn(pointer task) { + ubyte x = 1 + x++ + dispatch(task) + } + sub dispatch(pointer task) { + call(task) + } + sub task1() { + ubyte counter = 0 + counter++ + spawn(&task1) + } +} +""" + val errors = ErrorReporterForTests(keepMessagesAfterReporting = true) + compileText(Qemu68kTarget(), optimize = false, src, tempdir().toPath(), + writeAssembly = true, assemble = false, errors = errors) shouldBe null + errors.errors.size shouldBe 3 + val all = errors.errors.joinToString("\n") + all shouldContain "recursive subroutine" + all shouldContain "cannot be given a stack frame (its address is taken)" + all shouldContain "cannot be given a stack frame (it makes an indirect call)" + all shouldContain "conservatively assumed to reach any subroutine" + all shouldNotContain "it calls '" + } }) diff --git a/compiler/test/TestSubroutines.kt b/compiler/test/TestSubroutines.kt index a85a64c54..61aba94dd 100644 --- a/compiler/test/TestSubroutines.kt +++ b/compiler/test/TestSubroutines.kt @@ -456,4 +456,79 @@ main { }""" compileText(VMTarget(), false, src, outputDir, writeAssembly = false).shouldNotBeNull() } + test("on..goto to a subroutine with parameters is an error") { + val text = """ + main { + ubyte @shared index = 1 + sub start() { + on index goto (task1, task2) + } + sub task1() { + } + sub task2(ubyte param) { + } + }""" + val errors = ErrorReporterForTests() + compileText(C64Target(), false, text, outputDir, errors=errors, writeAssembly = false) shouldBe null + errors.errors.size shouldBe 1 + errors.errors[0] shouldContain "can't dispatch to a subroutine that takes parameters: task2" + } + + test("on..call to a subroutine with parameters is an error") { + val text = """ + main { + ubyte @shared index = 1 + sub start() { + on index call (task1, task2) + } + sub task1() { + } + sub task2(uword param) { + } + }""" + val errors = ErrorReporterForTests() + compileText(C64Target(), false, text, outputDir, errors=errors, writeAssembly = false) shouldBe null + errors.errors.size shouldBe 1 + errors.errors[0] shouldContain "can't dispatch to a subroutine that takes parameters: task2" + } + + test("jmptable entry with parameters is an error") { + val text = """ + main { + %jmptable (task1, task2) + sub start() { + } + sub task1() { + } + sub task2(ubyte param) { + } + }""" + val errors = ErrorReporterForTests() + compileText(C64Target(), false, text, outputDir, errors=errors, writeAssembly = false) shouldBe null + errors.errors.size shouldBe 1 + errors.errors[0] shouldContain "can't dispatch to a subroutine that takes parameters: task2" + } + + test("dispatch to subroutines without parameters is allowed") { + val text = """ + main { + ubyte @shared index = 1 + sub start() { + on index goto (task1, task2) + on index call (task3, task4) + } + sub task1() -> bool { + return true + } + sub task2() { + } + sub task3() { + } + sub task4() { + } + }""" + val errors = ErrorReporterForTests() + compileText(C64Target(), false, text, outputDir, errors=errors, writeAssembly = false).shouldNotBeNull() + errors.errors shouldBe emptyList() + } }) diff --git a/examples/test.p8 b/examples/test.p8 index db1216106..7a67dc927 100644 --- a/examples/test.p8 +++ b/examples/test.p8 @@ -1,47 +1,29 @@ %import textio -%import coroutines main { - %jmptable task1,task2 - sub start() { - void coroutines.add(task1, 0) - void coroutines.add(task2, 0) - void coroutines.add(task3, 0) - txt.print("starting 3 tasks...\n") - coroutines.run(supervisor) - txt.print("\ndone.\n") - } + ubyte @shared i = 1 - ubyte @shared count - - sub supervisor() -> bool { - count++ - if count==200 { - coroutines.killall() - return false - } - return true + on i goto (task1,task2,task3) } sub task1() { - repeat { - txt.chrout('a') - void coroutines.yield() - } + long @shared z1 = 11111 + txt.print("one ") + txt.print_l(z1) + txt.nl() } - sub task2() { - repeat { - txt.chrout('b') - void coroutines.yield() - } + long @shared z2 = 22222 + txt.print("two ") + txt.print_l(z2) + txt.nl() } - - sub task3() { - repeat { - txt.chrout('c') - void coroutines.yield() - } + sub task3() -> bool { + long @shared z3 = 33333 + txt.print("three ") + txt.print_l(z3) + txt.nl() + return false } }