Files
prog8/compiler/res/prog8lib/coroutines.p8
Irmen de Jong e8795859c5 added sorting library for target virtual
added sorting routines that sort a values array together with the keys array
optimized gnomesort a little
2025-06-07 19:42:40 +02:00

144 lines
6.6 KiB
Lua

; Cooperative multitasking / Coroutines
; EXPERIMENTAL LIBRARY: Api may change or it may be removed completely in a future version!
; Achieves cooperative multitasking among a list of tasks each calling yield() to pass control to the next.
; Uses cpu stack return address juggling to cycle between different tasks.
;
; Features:
; - can have a dynamic number of active tasks (max 64), when a task ends it is automatically removed from the task list.
; - you can add new tasks, while the rest is already running. Just not yet from inside IRQ handlers!
; - tasks are regular subroutines but have to call yield() to pass control to the next task (round-robin)
; - yield() returns the registered userdata value for that task, so a single subroutine could be used as multiple tasks on different userdata
; BUT!! in that case, the subroutine cannot have any variables of its own that keep state, because they're shared across the multiple tasks!
; (if a subroutine is just inserted as a task exactly ONCE, it's okay to use normal variables for state, because nobody will share them)
; - you can kill a task (if you know it's id...)
; - when all tasks are finished the run() call will also return.
; - tasks can't push anything on the cpu stack before calling yield() - that will cause chaos.
; - this library is not (yet) usable from IRQ handlers. Don't do it. It will end badly. (can't manipulate the task list simultaneously)
;
; Difference from IRQ handlers:
; - you can have many tasks instead of only 2 (main program + irq handler)
; - it's not tied to any IRQ setup, and will run as fast as the tasks themselves allow
; - tasks fully control the switch to the next task; there is no preemptive switching
; - tasks will need to save/restore their own state, maybe by useing the userdata (pointer?) and/or task id for that.
;
; USAGE:
; - call add(taskaddress) to add a new task. It returns the task id.
; - call run(supervisor) to start executing all tasks until none are left. Pass 0 or a pointer to a 'supervisor' routine.
; 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.
; - IMPORTANT: if you add the same subroutine multiple times, IT CANNOT DEPEND ON ANY LOCAL VARIABLES OR R0-R15 TO KEEP STATE. NOT EVEN REPEAT LOOP COUNTERS.
; Those are all shared in the different tasks! You HAVE to use a mechanism around the userdata value (pointer?) to keep separate state elsewhere!
; - IMPORTANT: ``defer`` cannot be used inside a coroutine that is reused for multiple tasks!!!
;
; TIP: HOW TO WAIT without BLOCKING other coroutines?
; Make sure you call yield() in the waiting loop, for example:
; uword timer = cbm.RDTIM16() + 60
; while cbm.RDTIM16() != timer
; void coroutines.yield()
coroutines {
%option ignore_unused
const ubyte MAX_TASKS = 64
uword[MAX_TASKS] tasklist
uword[MAX_TASKS] userdatas
ubyte active_task
uword supervisor
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] = sys.get_as_returnaddress(taskaddress)
userdatas[cx16.r0L] = userdata
sys.set_carry()
return cx16.r0L
}
}
; no space for new task
sys.clear_carry()
return 255
}
sub killall() {
; kill all existing tasks
for cx16.r0L in 0 to len(tasklist)-1 {
kill(cx16.r0L)
}
}
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)
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.
; 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()
skip_task:
if not next_task() {
void sys.popw() ; remove return to the termination handler
return 0 ; exiting here will now actually return back to the calling program that called run()
}
if supervisor!=0
if lsb(call(supervisor))==0
goto skip_task
; 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(tasklist)
active_task=0
task_return_address = tasklist[active_task]
if task_return_address!=0 {
return true
}
}
return false ; no task
}
}
sub kill(ubyte @nozp taskid) {
tasklist[taskid] = 0
}
sub current() -> ubyte {
return active_task
}
sub termination() {
; internal routine: a task has terminated. wipe it from the list.
kill(active_task)
; reactivate this termination handler and go to the next task
sys.push_returnaddress(&termination)
goto coroutines.yield.skip_task
}
}