feat: Phase 8.3 true non-blocking async/await with stackful coroutines

- C runtime: ucontext-based coroutines (bux_async_spawn/yield/await/run)
- Round-robin scheduler in bux_async_run() manages ready queue
- spawn Func() without args → bux_async_spawn() creates coroutine
- await → blocks/yields until coroutine completes
- async func → emitted as regular C function, runs on dedicated stack
- Interleaved execution: multiple coroutines yield cooperatively
- Example: examples/async.bux demonstrates WorkA/WorkB interleaving
- All 20 examples pass, selfhost build works
This commit is contained in:
2026-06-01 01:06:46 +03:00
parent d337ada4d6
commit 5ec755743d
4 changed files with 168 additions and 22 deletions
+26 -17
View File
@@ -1,23 +1,32 @@
import Std::Io::{PrintLine, PrintInt}; import Std::Io::PrintLine;
import Std::Task::TaskHandle;
async func BackgroundWork(id: int) { extern func bux_async_yield();
PrintLine("Task started:"); extern func bux_async_run();
PrintInt(id); extern func bux_async_spawn(fn: *void) -> *void;
PrintLine(""); extern func bux_async_await(handle: *void);
Task_Sleep(50);
PrintLine("Task done:"); async func WorkA() {
PrintInt(id); PrintLine("WorkA: step 1");
PrintLine(""); bux_async_yield();
PrintLine("WorkA: step 2");
bux_async_yield();
PrintLine("WorkA: done");
}
async func WorkB() {
PrintLine("WorkB: step 1");
bux_async_yield();
PrintLine("WorkB: step 2");
bux_async_yield();
PrintLine("WorkB: done");
} }
func Main() -> int { func Main() -> int {
PrintLine("Main: spawning tasks..."); PrintLine("Main: start");
let h1: *void = spawn BackgroundWork(1); let h1: *void = spawn WorkA();
let h2: *void = spawn BackgroundWork(2); let h2: *void = spawn WorkB();
PrintLine("Main: waiting for tasks..."); bux_async_await(h1);
h1.await; bux_async_await(h2);
h2.await; PrintLine("Main: all done");
PrintLine("Main: all tasks completed");
return 0; return 0;
} }
+4 -4
View File
@@ -253,14 +253,14 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
return &"sizeof({typ})" return &"sizeof({typ})"
of hSpawn: of hSpawn:
var argsStr = ""
if node.spawnArgs.len > 0: if node.spawnArgs.len > 0:
# Package arguments into a heap-allocated struct (simplified: single arg) # Fallback to OS thread spawn for functions with arguments
var argsStr = ""
let arg = be.emitExpr(node.spawnArgs[0]) let arg = be.emitExpr(node.spawnArgs[0])
argsStr = &"(void*){arg}" argsStr = &"(void*){arg}"
else:
argsStr = "NULL"
return &"bux_task_spawn({node.spawnCallee}, {argsStr})" return &"bux_task_spawn({node.spawnCallee}, {argsStr})"
else:
return &"bux_async_spawn({node.spawnCallee})"
of hIf: of hIf:
# Ternary expression # Ternary expression
+1 -1
View File
@@ -770,7 +770,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
of ekAwait: of ekAwait:
let lowered = ctx.lowerExpr(expr.exprAwaitOperand) let lowered = ctx.lowerExpr(expr.exprAwaitOperand)
return hirCall("bux_task_join", @[lowered], makeVoid(), loc) return hirCall("bux_async_await", @[lowered], makeVoid(), loc)
else: else:
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc), return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
+137
View File
@@ -7,6 +7,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <string.h> #include <string.h>
#include <pthread.h> #include <pthread.h>
#include <ucontext.h>
/* Command-line argument storage */ /* Command-line argument storage */
int g_argc = 0; int g_argc = 0;
@@ -794,3 +795,139 @@ void bux_channel_free(void* handle) {
free(ch->buffer); free(ch->buffer);
free(ch); free(ch);
} }
/* ============================================================================
* Stackful Coroutines + Async Scheduler (Phase 8.3 true async)
* ============================================================================ */
#define BUX_CORO_STACK_SIZE (64 * 1024)
typedef struct bux_async_task {
ucontext_t ctx;
ucontext_t* caller_ctx;
uint8_t* stack;
int state; /* 0 = ready, 1 = running, 2 = done */
void (*entry)(void);
struct bux_async_task* next;
} bux_async_task_t;
static bux_async_task_t* bux_ready_head = NULL;
static bux_async_task_t* bux_ready_tail = NULL;
static bux_async_task_t* bux_current_task = NULL;
static ucontext_t bux_scheduler_ctx;
static int bux_scheduler_running = 0;
static void bux_enqueue_ready(bux_async_task_t* task) {
task->next = NULL;
if (bux_ready_tail) {
bux_ready_tail->next = task;
} else {
bux_ready_head = task;
}
bux_ready_tail = task;
}
static bux_async_task_t* bux_dequeue_ready(void) {
bux_async_task_t* task = bux_ready_head;
if (task) {
bux_ready_head = task->next;
if (!bux_ready_head) bux_ready_tail = NULL;
}
return task;
}
static void bux_coro_trampoline(void) {
bux_async_task_t* self = bux_current_task;
if (self != NULL && self->entry != NULL) {
self->entry();
}
if (self != NULL) {
self->state = 2; /* done */
swapcontext(&self->ctx, &bux_scheduler_ctx);
}
}
void* bux_async_spawn(void (*func)(void)) {
bux_async_task_t* task = (bux_async_task_t*)malloc(sizeof(bux_async_task_t));
if (!task) {
fprintf(stderr, "bux runtime: out of memory (async spawn)\n");
abort();
}
task->stack = (uint8_t*)malloc(BUX_CORO_STACK_SIZE);
if (!task->stack) {
fprintf(stderr, "bux runtime: out of memory (coro stack)\n");
free(task);
abort();
}
task->state = 0;
task->next = NULL;
task->caller_ctx = NULL;
task->entry = func;
getcontext(&task->ctx);
task->ctx.uc_stack.ss_sp = task->stack;
task->ctx.uc_stack.ss_size = BUX_CORO_STACK_SIZE;
task->ctx.uc_link = &bux_scheduler_ctx;
makecontext(&task->ctx, bux_coro_trampoline, 0);
bux_enqueue_ready(task);
return task;
}
void bux_async_yield(void) {
if (bux_current_task != NULL) {
bux_async_task_t* task = bux_current_task;
bux_current_task = NULL;
bux_enqueue_ready(task);
swapcontext(&task->ctx, &bux_scheduler_ctx);
}
}
void bux_async_await(void* handle) {
if (!handle) return;
bux_async_task_t* target = (bux_async_task_t*)handle;
while (target->state != 2) {
if (bux_current_task != NULL) {
/* Inside a coroutine: yield and let scheduler run */
bux_async_yield();
} else {
/* Main thread: run scheduler until target is done */
if (!bux_scheduler_running) {
bux_async_run();
}
}
}
}
void bux_async_run(void) {
if (bux_scheduler_running) return;
bux_scheduler_running = 1;
getcontext(&bux_scheduler_ctx);
while (bux_ready_head != NULL) {
bux_async_task_t* task = bux_dequeue_ready();
if (!task) break;
if (task->state == 2) {
free(task->stack);
free(task);
continue;
}
task->state = 1;
bux_current_task = task;
swapcontext(&bux_scheduler_ctx, &task->ctx);
bux_current_task = NULL;
if (task->state == 2) {
free(task->stack);
free(task);
}
}
bux_scheduler_running = 0;
}
void bux_async_sleep(int64_t ms) {
/* Naive sleep: block this coroutine via yield */
if (ms > 0) {
/* TODO: proper timer-based scheduling */
/* For now, just yield once */
bux_async_yield();
}
}