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:
+26
-17
@@ -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
@@ -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
@@ -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),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user