added async and hashmap lib to the stdlib as well as tests for them, improved the performance of the compiler, improved the buildt in doc tool and fixed some other small issues

This commit is contained in:
igor
2026-03-11 14:41:40 +01:00
parent 4e51e8803f
commit 0ac7f26a18
7 changed files with 3377 additions and 265 deletions

447
stdlib/async.sl Normal file
View File

@@ -0,0 +1,447 @@
# 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
} ;