From 5ec755743ddc513ae4abf2593a2d3d26f5b3cfdb Mon Sep 17 00:00:00 2001 From: dimgigov Date: Mon, 1 Jun 2026 01:06:46 +0300 Subject: [PATCH] feat: Phase 8.3 true non-blocking async/await with stackful coroutines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- examples/async.bux | 43 ++++++++------ src/c_backend.nim | 8 +-- src/hir_lower.nim | 2 +- stdlib/runtime.c | 137 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 22 deletions(-) diff --git a/examples/async.bux b/examples/async.bux index 4295a3c..08cafcc 100644 --- a/examples/async.bux +++ b/examples/async.bux @@ -1,23 +1,32 @@ -import Std::Io::{PrintLine, PrintInt}; -import Std::Task::TaskHandle; +import Std::Io::PrintLine; -async func BackgroundWork(id: int) { - PrintLine("Task started:"); - PrintInt(id); - PrintLine(""); - Task_Sleep(50); - PrintLine("Task done:"); - PrintInt(id); - PrintLine(""); +extern func bux_async_yield(); +extern func bux_async_run(); +extern func bux_async_spawn(fn: *void) -> *void; +extern func bux_async_await(handle: *void); + +async func WorkA() { + PrintLine("WorkA: step 1"); + 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 { - PrintLine("Main: spawning tasks..."); - let h1: *void = spawn BackgroundWork(1); - let h2: *void = spawn BackgroundWork(2); - PrintLine("Main: waiting for tasks..."); - h1.await; - h2.await; - PrintLine("Main: all tasks completed"); + PrintLine("Main: start"); + let h1: *void = spawn WorkA(); + let h2: *void = spawn WorkB(); + bux_async_await(h1); + bux_async_await(h2); + PrintLine("Main: all done"); return 0; } diff --git a/src/c_backend.nim b/src/c_backend.nim index 63b8e85..eca6a71 100644 --- a/src/c_backend.nim +++ b/src/c_backend.nim @@ -253,14 +253,14 @@ proc emitExpr(be: var CBackend, node: HirNode): string = return &"sizeof({typ})" of hSpawn: - var argsStr = "" 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]) argsStr = &"(void*){arg}" + return &"bux_task_spawn({node.spawnCallee}, {argsStr})" else: - argsStr = "NULL" - return &"bux_task_spawn({node.spawnCallee}, {argsStr})" + return &"bux_async_spawn({node.spawnCallee})" of hIf: # Ternary expression diff --git a/src/hir_lower.nim b/src/hir_lower.nim index c74d208..83ad7ff 100644 --- a/src/hir_lower.nim +++ b/src/hir_lower.nim @@ -770,7 +770,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = of ekAwait: let lowered = ctx.lowerExpr(expr.exprAwaitOperand) - return hirCall("bux_task_join", @[lowered], makeVoid(), loc) + return hirCall("bux_async_await", @[lowered], makeVoid(), loc) else: return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc), diff --git a/stdlib/runtime.c b/stdlib/runtime.c index cad6aa8..c6f6843 100644 --- a/stdlib/runtime.c +++ b/stdlib/runtime.c @@ -7,6 +7,7 @@ #include #include #include +#include /* Command-line argument storage */ int g_argc = 0; @@ -794,3 +795,139 @@ void bux_channel_free(void* handle) { free(ch->buffer); 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(); + } +}