From ddceec364e71203fdd6111b701380a0c4a33ee56 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Wed, 4 Jun 2025 20:53:21 +0200 Subject: [PATCH] optimized coroutines library --- compiler/res/prog8lib/coroutines.p8 | 62 +++++++++++----------------- compiler/res/prog8lib/cx16/syslib.p8 | 12 ++++++ docs/source/todo.rst | 1 + 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/compiler/res/prog8lib/coroutines.p8 b/compiler/res/prog8lib/coroutines.p8 index 4dbb202a2..14c07af22 100644 --- a/compiler/res/prog8lib/coroutines.p8 +++ b/compiler/res/prog8lib/coroutines.p8 @@ -28,6 +28,8 @@ ; that routine can for instance call current() (or just look at the active_task variable) to get the id of the next task to execute. ; It has then to return a boolean: true=next task is to be executed, false=skip the task this time. ; - in tasks: call yield() to pass control to the next task. Use the returned userdata value to do different things. +; For now, you MUST call yield() only from the actual subroutine that has been registered as a task! +; (this is because otherwise the cpu call stack gets messed up and an RTS in task1 could suddenly pop a return address belonging to another tasks' call frame) ; - call current() to get the current task id. ; - call kill(taskid) to kill a task by id. ; - call killall() to kill all tasks. @@ -41,19 +43,17 @@ coroutines { const ubyte MAX_TASKS = 64 uword[MAX_TASKS] tasklist uword[MAX_TASKS] userdatas - uword[MAX_TASKS] returnaddresses ubyte active_task uword supervisor - sub add(uword taskaddress, uword userdata) -> ubyte { + sub add(uword @nozp taskaddress, uword @nozp userdata) -> ubyte { ; find the next empty slot in the tasklist and stick it there ; returns the task id of the new task, or 255 if there was no space for more tasks. 0 is a valid task id! ; also returns the success in the Carry flag (carry set=success, carry clear = task was not added) for cx16.r0L in 0 to len(tasklist)-1 { if tasklist[cx16.r0L] == 0 { - tasklist[cx16.r0L] = taskaddress + tasklist[cx16.r0L] = sys.get_as_returnaddress(taskaddress) userdatas[cx16.r0L] = userdata - returnaddresses[cx16.r0L] = 0 sys.set_carry() return cx16.r0L } @@ -70,57 +70,48 @@ coroutines { } } - sub run(uword supervisor_routine) { + sub run(uword @nozp supervisor_routine) { supervisor = supervisor_routine for active_task in 0 to len(tasklist)-1 { if tasklist[active_task]!=0 { ; activate the termination handler and start the first task ; note: cannot use pushw() because JSR doesn't push the return address in the same way sys.push_returnaddress(&termination) - goto tasklist[active_task] + sys.pushw(tasklist[active_task]) + return } } } sub yield() -> uword { - ; Store the return address of the yielding task, - ; and continue with the next one instead (round-robin) - ; Returns the associated userdata value - uword task_start, task_continue - returnaddresses[active_task] = sys.popw() + ; Store the return address of the yielding task, and continue with the next one instead (round-robin) + ; Returns the associated userdata value. + ; NOTE: CAN ONLY BE CALLED FROM THE SCOPE OF THE SUBROUTINE THAT HAS BEEN REGISTERED AS THE TASK! + uword task_return_address + tasklist[active_task] = sys.popw() -resume_with_next_task: +skip_task: if not next_task() { void sys.popw() ; remove return to the termination handler - return 0 ; exiting here will now actually return from the start() call back to the calling program :) + return 0 ; exiting here will now actually return back to the calling program that called run() } - if supervisor!=0 { + if supervisor!=0 if lsb(call(supervisor))==0 - goto resume_with_next_task - } + goto skip_task - if task_continue==0 { - ; fetch start address of next task. - ; address on the stack must be pushed in reverse byte order - ; also, subtract 1 from the start address because JSR pushes returnaddress minus 1 - ; note: cannot use pushw() because JSR doesn't push the return address in the same way - sys.push_returnaddress(task_start) - } else - sys.pushw(task_continue) - - ; returning from yield then continues with the next coroutine + ; returning from yield then continues with the next coroutine: + sys.pushw(task_return_address) return userdatas[active_task] sub next_task() -> bool { ; search through the task list for the next active task repeat len(tasklist) { active_task++ - if active_task==len(returnaddresses) + if active_task==len(tasklist) active_task=0 - task_start = tasklist[active_task] - if task_start!=0 { - task_continue = returnaddresses[active_task] + task_return_address = tasklist[active_task] + if task_return_address!=0 { return true } } @@ -128,9 +119,8 @@ resume_with_next_task: } } - sub kill(ubyte taskid) { + sub kill(ubyte @nozp taskid) { tasklist[taskid] = 0 - returnaddresses[taskid] = 0 } sub current() -> ubyte { @@ -138,12 +128,10 @@ resume_with_next_task: } sub termination() { - ; a task has terminated. wipe it from the list. - ; this is an internal routine + ; internal routine: a task has terminated. wipe it from the list. kill(active_task) - ; reactivate this termination handler - ; note: cannot use pushw() because JSR doesn't push the return address in the same way + ; reactivate this termination handler and go to the next task sys.push_returnaddress(&termination) - goto coroutines.yield.resume_with_next_task + goto coroutines.yield.skip_task } } diff --git a/compiler/res/prog8lib/cx16/syslib.p8 b/compiler/res/prog8lib/cx16/syslib.p8 index 9211e0072..df3a4bfbc 100644 --- a/compiler/res/prog8lib/cx16/syslib.p8 +++ b/compiler/res/prog8lib/cx16/syslib.p8 @@ -2028,6 +2028,18 @@ save_SCRATCH_ZPWORD2 .word ? }} } + asmsub get_as_returnaddress(uword address @XY) -> uword @AX { + %asm {{ + ; return the address like JSR would push onto the stack: address-1, MSB first then LSB + cpx #0 + bne + + dey ++ dex + tya + rts + }} + } + inline asmsub pop() -> ubyte @A { %asm {{ pla diff --git a/docs/source/todo.rst b/docs/source/todo.rst index 3b5029575..26cee1f34 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -10,6 +10,7 @@ Idea is to make it feature complete in the IR/Virtual target, then merge it to m Future Things and Ideas ^^^^^^^^^^^^^^^^^^^^^^^ +- remove (experimental) tag in docs from 3 libraries - is "checkAssignmentCompatible" redundant (gets called just 1 time!) when we also have "checkValueTypeAndRange" ? - enums? - romable: should we have a way to explicitly set the memory address for the BSS area (instead of only the highram bank number on X16, allow a memory address too for the -varshigh option?)