448 lines
12 KiB
Plaintext
448 lines
12 KiB
Plaintext
# Async — Cooperative coroutine scheduler
|
|
#
|
|
# Provides lightweight cooperative multitasking built on context switching.
|
|
# Each task has its own data stack; the scheduler round-robins between
|
|
# ready tasks whenever `yield` is called.
|
|
#
|
|
# Task layout at address `task`:
|
|
# [task + 0] status (qword) 0=ready, 1=running, 2=done
|
|
# [task + 8] data_sp (qword) saved data stack pointer (r12)
|
|
# [task + 16] ret_sp (qword) saved return address for resume
|
|
# [task + 24] stack_base (qword) base of allocated stack buffer
|
|
# [task + 32] stack_size (qword) size of allocated stack buffer
|
|
# [task + 40] entry_fn (qword) pointer to the word to execute
|
|
#
|
|
# Scheduler layout at address `sched`:
|
|
# [sched + 0] task_count (qword)
|
|
# [sched + 8] current_idx (qword)
|
|
# [sched + 16] tasks_ptr (qword) pointer to array of task pointers
|
|
# [sched + 24] main_sp (qword) saved main data stack pointer
|
|
# [sched + 32] main_ret (qword) saved main return address
|
|
#
|
|
# Usage:
|
|
# 16 sched_new # create scheduler with capacity 16
|
|
# &my_worker1 sched_spawn # spawn task running my_worker1
|
|
# &my_worker2 sched_spawn # spawn task running my_worker2
|
|
# sched_run # run all tasks to completion
|
|
# sched_free # clean up
|
|
#
|
|
# Inside a task word, call `yield` to yield to the next ready task.
|
|
|
|
import mem.sl
|
|
|
|
# ── Constants ─────────────────────────────────────────────────
|
|
|
|
# Default per-task stack size: 8 KiB
|
|
macro ASYNC_STACK_SIZE 0 8192 ;
|
|
|
|
# Task status values
|
|
macro TASK_READY 0 0 ;
|
|
macro TASK_RUNNING 0 1 ;
|
|
macro TASK_DONE 0 2 ;
|
|
|
|
# ── Task accessors ────────────────────────────────────────────
|
|
|
|
#task_status [* | task] -> [* | status]
|
|
word task_status @ end
|
|
|
|
#task_set_status [*, task | status] -> [*]
|
|
word task_set_status ! end
|
|
|
|
#task_data_sp [* | task] -> [* | sp]
|
|
word task_data_sp 8 + @ end
|
|
|
|
#task_set_data_sp [*, task | sp] -> [*]
|
|
word task_set_data_sp swap 8 + swap ! end
|
|
|
|
#task_ret_sp [* | task] -> [* | ret]
|
|
word task_ret_sp 16 + @ end
|
|
|
|
#task_set_ret_sp [*, task | ret] -> [*]
|
|
word task_set_ret_sp swap 16 + swap ! end
|
|
|
|
#task_stack_base [* | task] -> [* | base]
|
|
word task_stack_base 24 + @ end
|
|
|
|
#task_stack_size [* | task] -> [* | size]
|
|
word task_stack_size 32 + @ end
|
|
|
|
#task_entry_fn [* | task] -> [* | fn_ptr]
|
|
word task_entry_fn 40 + @ end
|
|
|
|
# ── Scheduler accessors ──────────────────────────────────────
|
|
|
|
#sched_task_count [* | sched] -> [* | n]
|
|
word sched_task_count @ end
|
|
|
|
#sched_current_idx [* | sched] -> [* | idx]
|
|
word sched_current_idx 8 + @ end
|
|
|
|
#sched_set_current_idx [*, sched | idx] -> [*]
|
|
word sched_set_current_idx swap 8 + swap ! end
|
|
|
|
#sched_tasks_ptr [* | sched] -> [* | ptr]
|
|
word sched_tasks_ptr 16 + @ end
|
|
|
|
#sched_main_sp [* | sched] -> [* | sp]
|
|
word sched_main_sp 24 + @ end
|
|
|
|
#sched_main_ret [* | sched] -> [* | ret]
|
|
word sched_main_ret 32 + @ end
|
|
|
|
# ── Global scheduler pointer (one active at a time) ──────────
|
|
|
|
# We store the current scheduler pointer in a global cell
|
|
# accessible via `mem`. Offset 0 of persistent buffer = scheduler ptr.
|
|
|
|
#__async_sched_ptr [*] -> [* | ptr]
|
|
# Get the global scheduler pointer
|
|
word __async_sched_ptr
|
|
mem @
|
|
end
|
|
|
|
#__async_set_sched_ptr [* | sched] -> [*]
|
|
# Set the global scheduler pointer
|
|
word __async_set_sched_ptr
|
|
mem swap !
|
|
end
|
|
|
|
# ── Task creation ─────────────────────────────────────────────
|
|
|
|
#task_new [* | fn_ptr] -> [* | task]
|
|
# Create a new task that will execute the given word.
|
|
word task_new
|
|
>r # save fn_ptr; R: [fn_ptr]; stack: [*]
|
|
|
|
# Allocate task struct (48 bytes)
|
|
48 alloc # stack: [* | task]
|
|
|
|
# Allocate task stack
|
|
ASYNC_STACK_SIZE alloc >r # R: [fn_ptr, stk_base]; stack: [* | task]
|
|
|
|
# status = READY (0)
|
|
dup 0 !
|
|
|
|
# stack_base = stk_base
|
|
r@ over 24 + swap !
|
|
|
|
# stack_size = ASYNC_STACK_SIZE
|
|
ASYNC_STACK_SIZE over 32 + swap !
|
|
|
|
# data_sp = stk_base + ASYNC_STACK_SIZE - 8 (top of stack, aligned)
|
|
r@ ASYNC_STACK_SIZE + 8 - over 8 + swap !
|
|
|
|
# ret_sp = 0 (not yet started)
|
|
dup 16 + 0 !
|
|
|
|
# entry_fn = fn_ptr
|
|
rdrop r> over 40 + swap !
|
|
end
|
|
|
|
#task_free [* | task] -> [*]
|
|
# Free a task and its stack buffer.
|
|
word task_free
|
|
dup task_stack_base over task_stack_size free
|
|
48 free
|
|
end
|
|
|
|
# ── Scheduler creation ───────────────────────────────────────
|
|
|
|
#sched_new [* | max_tasks] -> [* | sched]
|
|
# Create a new scheduler with room for max_tasks.
|
|
word sched_new
|
|
# Allocate scheduler struct (40 bytes)
|
|
40 alloc # stack: [*, max_tasks | sched]
|
|
|
|
# task_count = 0
|
|
dup 0 !
|
|
|
|
# current_idx = 0
|
|
dup 8 + 0 !
|
|
|
|
# Allocate tasks pointer array (max_tasks * 8)
|
|
over 8 * alloc
|
|
over 16 + over ! drop # sched.tasks_ptr = array
|
|
|
|
# main_sp = 0 (set when run starts)
|
|
dup 24 + 0 !
|
|
|
|
# main_ret = 0
|
|
dup 32 + 0 !
|
|
|
|
nip
|
|
end
|
|
|
|
#sched_free [* | sched] -> [*]
|
|
# Free the scheduler and all its tasks.
|
|
word sched_free
|
|
# Free each task
|
|
dup sched_task_count
|
|
0
|
|
while 2dup > do
|
|
2 pick sched_tasks_ptr over 8 * + @
|
|
task_free
|
|
1 +
|
|
end
|
|
2drop
|
|
|
|
40 free
|
|
end
|
|
|
|
# ── Spawning tasks ────────────────────────────────────────────
|
|
|
|
#sched_spawn [*, sched | fn_ptr] -> [* | sched]
|
|
# Spawn a new task in the scheduler.
|
|
word sched_spawn
|
|
task_new >r # save task; R:[task]; stack: [* | sched]
|
|
|
|
# Store task at tasks_ptr[count]
|
|
dup sched_tasks_ptr over @ 8 * + # [sched, &tasks[count]]
|
|
r@ ! # tasks[count] = task
|
|
|
|
# Increment task_count
|
|
dup @ 1 + over swap !
|
|
|
|
rdrop
|
|
end
|
|
|
|
# ── Context switch (the core of async) ───────────────────────
|
|
|
|
#yield [*] -> [*]
|
|
# Yield execution to the next ready task.
|
|
# Saves current data stack pointer, restores the next task's.
|
|
:asm yield {
|
|
; Save current r12 (data stack pointer) into current task
|
|
; Load scheduler pointer from mem (persistent buffer)
|
|
lea rax, [rel persistent]
|
|
mov rax, [rax] ; sched ptr
|
|
|
|
; Get current_idx
|
|
mov rbx, [rax + 8] ; current_idx
|
|
mov rcx, [rax + 16] ; tasks_ptr
|
|
mov rdx, [rcx + rbx*8] ; current task ptr
|
|
|
|
; Save r12 into task.data_sp
|
|
mov [rdx + 8], r12
|
|
|
|
; Save return address: caller's return is on the x86 stack
|
|
; We pop it and save it in task.ret_sp
|
|
pop rsi ; return address
|
|
mov [rdx + 16], rsi
|
|
|
|
; Mark current task as READY (it was RUNNING)
|
|
mov qword [rdx], 0 ; TASK_READY
|
|
|
|
; Find next ready task (round-robin)
|
|
mov r8, [rax] ; task_count
|
|
mov r9, rbx ; start from current_idx
|
|
.find_next:
|
|
inc r9
|
|
cmp r9, r8
|
|
jl .no_wrap
|
|
xor r9, r9 ; wrap to 0
|
|
.no_wrap:
|
|
cmp r9, rbx
|
|
je .no_other ; looped back: only one task
|
|
|
|
mov r10, [rcx + r9*8] ; candidate task
|
|
mov r11, [r10] ; status
|
|
cmp r11, 0 ; TASK_READY?
|
|
je .found_task
|
|
jmp .find_next
|
|
|
|
.no_other:
|
|
; Only one ready task (self): re-schedule self
|
|
mov r10, rdx
|
|
mov r9, rbx
|
|
|
|
.found_task:
|
|
; Update scheduler current_idx
|
|
mov [rax + 8], r9
|
|
|
|
; Mark new task as RUNNING
|
|
mov qword [r10], 1
|
|
|
|
; Check if task has a saved return address (non-zero means resumed)
|
|
mov rsi, [r10 + 16]
|
|
cmp rsi, 0
|
|
je .first_run
|
|
|
|
; Resume: restore data stack and jump to saved return address
|
|
mov r12, [r10 + 8]
|
|
push rsi
|
|
ret
|
|
|
|
.first_run:
|
|
; First run: set up data stack and call entry function
|
|
mov r12, [r10 + 8] ; task's data stack
|
|
|
|
; Save our scheduler info so the task can find it
|
|
; The task entry function needs no args — it uses the stack.
|
|
|
|
; Get entry function pointer
|
|
mov rdi, [r10 + 40]
|
|
|
|
; When the entry returns, we need to mark it done and yield
|
|
; Push a return address that handles cleanup
|
|
lea rsi, [rel .task_done]
|
|
push rsi
|
|
jmp rdi ; tail-call into task entry
|
|
|
|
.task_done:
|
|
; Task finished: mark as DONE
|
|
lea rax, [rel persistent]
|
|
mov rax, [rax] ; sched ptr
|
|
mov rbx, [rax + 8] ; current_idx
|
|
mov rcx, [rax + 16] ; tasks_ptr
|
|
mov rdx, [rcx + rbx*8] ; current task
|
|
mov qword [rdx], 2 ; TASK_DONE
|
|
|
|
; Find next ready task
|
|
mov r8, [rax] ; task_count
|
|
mov r9, rbx
|
|
.find_next2:
|
|
inc r9
|
|
cmp r9, r8
|
|
jl .no_wrap2
|
|
xor r9, r9
|
|
.no_wrap2:
|
|
cmp r9, rbx
|
|
je .all_done ; no more tasks
|
|
|
|
mov r10, [rcx + r9*8]
|
|
mov r11, [r10]
|
|
cmp r11, 0 ; TASK_READY?
|
|
je .found_task2
|
|
cmp r11, 1 ; TASK_RUNNING? (shouldn't happen)
|
|
je .found_task2
|
|
jmp .find_next2
|
|
|
|
.all_done:
|
|
; All tasks done: restore main context
|
|
mov r12, [rax + 24] ; main_sp
|
|
mov rsi, [rax + 32] ; main_ret
|
|
push rsi
|
|
ret
|
|
|
|
.found_task2:
|
|
mov [rax + 8], r9
|
|
mov qword [r10], 1
|
|
mov rsi, [r10 + 16]
|
|
cmp rsi, 0
|
|
je .first_run2
|
|
mov r12, [r10 + 8]
|
|
push rsi
|
|
ret
|
|
|
|
.first_run2:
|
|
mov r12, [r10 + 8]
|
|
mov rdi, [r10 + 40]
|
|
lea rsi, [rel .task_done]
|
|
push rsi
|
|
jmp rdi
|
|
} ;
|
|
|
|
# ── Scheduler run ─────────────────────────────────────────────
|
|
|
|
#sched_run [* | sched] -> [* | sched]
|
|
# Run all spawned tasks to completion.
|
|
# Saves the main context and starts the first task.
|
|
:asm sched_run {
|
|
mov rax, [r12] ; sched ptr (peek, keep on data stack)
|
|
|
|
; Store as global scheduler
|
|
lea rbx, [rel persistent]
|
|
mov [rbx], rax
|
|
|
|
; Save main data stack pointer (sched still on stack)
|
|
mov [rax + 24], r12
|
|
|
|
; Save main return address (where to come back)
|
|
pop rsi
|
|
mov [rax + 32], rsi
|
|
|
|
; Find first ready task
|
|
mov r8, [rax] ; task_count
|
|
cmp r8, 0
|
|
je .no_tasks
|
|
|
|
mov rcx, [rax + 16] ; tasks_ptr
|
|
xor r9, r9 ; idx = 0
|
|
.scan:
|
|
cmp r9, r8
|
|
jge .no_tasks
|
|
mov r10, [rcx + r9*8]
|
|
mov r11, [r10]
|
|
cmp r11, 0 ; TASK_READY?
|
|
je .start
|
|
inc r9
|
|
jmp .scan
|
|
|
|
.start:
|
|
mov [rax + 8], r9 ; set current_idx
|
|
mov qword [r10], 1 ; TASK_RUNNING
|
|
mov r12, [r10 + 8] ; task's data stack
|
|
|
|
mov rdi, [r10 + 40] ; entry function
|
|
lea rsi, [rel .task_finished]
|
|
push rsi
|
|
jmp rdi
|
|
|
|
.task_finished:
|
|
; Task returned — mark done and find next
|
|
lea rax, [rel persistent]
|
|
mov rax, [rax]
|
|
mov rbx, [rax + 8]
|
|
mov rcx, [rax + 16]
|
|
mov rdx, [rcx + rbx*8]
|
|
mov qword [rdx], 2 ; TASK_DONE
|
|
|
|
mov r8, [rax]
|
|
mov r9, rbx
|
|
.find_next_run:
|
|
inc r9
|
|
cmp r9, r8
|
|
jl .no_wrap_run
|
|
xor r9, r9
|
|
.no_wrap_run:
|
|
cmp r9, rbx
|
|
je .all_done_run
|
|
|
|
mov r10, [rcx + r9*8]
|
|
mov r11, [r10]
|
|
cmp r11, 0
|
|
je .found_run
|
|
jmp .find_next_run
|
|
|
|
.all_done_run:
|
|
; Restore main context
|
|
mov r12, [rax + 24]
|
|
mov rsi, [rax + 32]
|
|
push rsi
|
|
ret
|
|
|
|
.found_run:
|
|
mov [rax + 8], r9
|
|
mov qword [r10], 1
|
|
mov rsi, [r10 + 16]
|
|
cmp rsi, 0
|
|
je .first_run_entry
|
|
mov r12, [r10 + 8]
|
|
push rsi
|
|
ret
|
|
|
|
.first_run_entry:
|
|
mov r12, [r10 + 8]
|
|
mov rdi, [r10 + 40]
|
|
lea rsi, [rel .task_finished]
|
|
push rsi
|
|
jmp rdi
|
|
|
|
.no_tasks:
|
|
; Nothing to run — restore and return
|
|
mov r12, [rax + 24]
|
|
mov rsi, [rax + 32]
|
|
push rsi
|
|
ret
|
|
} ;
|