xco

Concurrency for C
git clone https://git.ryansepassi.com/git/xco.git
Log | Files | Refs

commit 7cc24269a31cc36717b7d32f3dfcb3218c9ddca2
parent ec177b0d2ad8ed0c7ac11b3ee08836bbd0b41169
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue,  5 May 2026 09:25:18 -0700

Unify xco and xstep outer API: drive coroutines through xstep directly

Remove xco_resume, xco_status, and xco_task_resume — they were pure
forwards to xstep / xstep_status on the embedded base. Callers now use
xstep(&c->base, v) and xstep_status(&c->base) for both coroutines and
hand-coded state machines, making the substrate unification visible
at the call site. xco_spawn / xco_task_spawn / xco_init / xco_suspend
stay because they have shapes xstep can't express (stacks, inner-side
suspension).

New test drives an xco_task_t through xstep directly to confirm the
trampoline still wires task_done from the body's return — i.e. the
unification holds at the task layer too.

Diffstat:
Mtests/test_xco.c | 43++++++++++++++++++++++++++++++++-----------
Mxco.c | 15++++++++-------
Mxco.h | 40++++++++++++++--------------------------
3 files changed, 54 insertions(+), 44 deletions(-)

diff --git a/tests/test_xco.c b/tests/test_xco.c @@ -89,12 +89,13 @@ static uintptr_t outer(uintptr_t arg) { /* Yield the inner coroutine's first value back to our resumer. */ uintptr_t v = xco_suspend(r.value); assert(v == 42); - /* Drive inner to completion. */ - r = xco_resume(&inner, 100); + /* Drive inner to completion via the generic xstep — same surface + * used for hand-coded state machines. */ + r = xstep(&inner.base, 100); assert(r.status == XSTEP_SUSPENDED && r.value == 12); - r = xco_resume(&inner, 200); + r = xstep(&inner.base, 200); assert(r.status == XSTEP_SUSPENDED && r.value == 13); - r = xco_resume(&inner, 300); + r = xstep(&inner.base, 300); assert(r.status == XSTEP_DEAD && r.value == 14); return 999; } @@ -141,8 +142,8 @@ static void test_xco_yield_alternates(void) { /* Drain the runtime: each yielder runs another step, yields again, * until both reach DEAD. */ rt_run(&rt, 0); - assert(xco_status(&c1) == XSTEP_DEAD); - assert(xco_status(&c2) == XSTEP_DEAD); + assert(xstep_status(&c1.base) == XSTEP_DEAD); + assert(xstep_status(&c2.base) == XSTEP_DEAD); /* Trace alternates: 1 2 1 2 1 2 (FIFO ready-queue ordering). */ assert(trace_len == 6); @@ -271,23 +272,42 @@ static void test_xco_task_cancel(void) { assert(v == 0); /* cancelled */ } +/* Unification at the task layer: drive an xco_task_t through xstep + * directly, bypassing xco_task_spawn. The trampoline still wires + * task_done from the body's return, because that lives inside xco_step, + * not in any spawn helper. */ +static void test_xco_task_via_xstep(void) { + xco_task_t xt; + xco_task_init(&xt, task_body_simple, stack_t1, STACK_BYTES); + assert(xstep_status(&xt.co.base) == XSTEP_INIT); + + xstep_result_t r = xstep(&xt.co.base, 41); + assert(r.status == XSTEP_DEAD); + assert(r.value == 42); + assert(task_finished(&xt.task)); + + uintptr_t v; + assert(event_try(task_done_event(&xt.task), &v)); + assert(v == 42); +} + int main(void) { assert(xco_self() == NULL); xco_t c; xco_init(&c, outer, stack_a, STACK_BYTES); - assert(xco_status(&c) == XSTEP_INIT); + assert(xstep_status(&c.base) == XSTEP_INIT); - xstep_result_t r = xco_resume(&c, 0); + xstep_result_t r = xstep(&c.base, 0); assert(xco_self() == NULL); assert(r.status == XSTEP_SUSPENDED); assert(r.value == 11); - assert(xco_status(&c) == XSTEP_SUSPENDED); + assert(xstep_status(&c.base) == XSTEP_SUSPENDED); - r = xco_resume(&c, 42); + r = xstep(&c.base, 42); assert(r.status == XSTEP_DEAD); assert(r.value == 999); - assert(xco_status(&c) == XSTEP_DEAD); + assert(xstep_status(&c.base) == XSTEP_DEAD); /* Unification: the same generic driver pumps a coroutine and a * hand-coded state machine, both reaching XSTEP_DEAD with the @@ -302,6 +322,7 @@ int main(void) { test_xco_yield_alternates(); test_xco_task_join_inline(); + test_xco_task_via_xstep(); test_xco_task_join_via_runtime(); test_xco_task_joined_by_other_task(); test_xco_task_cancel(); diff --git a/xco.c b/xco.c @@ -8,8 +8,8 @@ * xco_self() TLS pointer — lives here. * * The xstep_fn registered into base.step (xco_step) is the entry point - * generic xstep callers see; xco_resume in the header is just a typed - * forward to xstep(). + * all callers see — coroutines are driven through xstep() exactly like + * any other xstep_t. * * This translation unit is architecture-neutral. The platform context * type is forward-declared (in xco_platform.h) and only ever referred @@ -51,9 +51,9 @@ static inline xco_platform_ctx_t *main_ctx(void) { /* Trampoline: runs on the coroutine's own stack, invoked by the * platform layer on the first switch into a fresh context. The - * argument is the value passed to that first xco_resume. The - * coroutine identifies itself via t_current, set by the resumer just - * before switching. */ + * argument is the value passed to that first xstep. The coroutine + * identifies itself via t_current, set by the resumer just before + * switching. */ static void trampoline(uintptr_t arg) { xco_impl_t *self = t_current; uintptr_t ret = self->fn(arg); @@ -63,8 +63,9 @@ static void trampoline(uintptr_t arg) { __builtin_unreachable(); } -/* xstep_fn entry point wired into base.step at init time. Generic - * xstep callers route through here; xco_resume is a typed forward. */ +/* xstep_fn entry point wired into base.step at init time. All callers + * — generic xstep consumers and xco-aware code alike — route through + * here. */ static xstep_result_t xco_step(xstep_t *s, uintptr_t value) { xco_impl_t *next = (xco_impl_t *)s; assert(next->base.status == XSTEP_INIT || next->base.status == XSTEP_SUSPENDED); diff --git a/xco.h b/xco.h @@ -33,8 +33,8 @@ #include "xco_arch.h" /* Coroutine entry point. The argument is the value supplied to the - * first xco_resume. The return value is delivered to the resumer as - * the final xstep_result, with status XSTEP_DEAD. */ + * first xstep on this xco. The return value is delivered to the resumer + * as the final xstep_result, with status XSTEP_DEAD. */ typedef uintptr_t (*xco_fn)(uintptr_t arg); /* Coroutine control block. Allocate anywhere — on a stack, in a @@ -63,25 +63,19 @@ uintptr_t xco_suspend(uintptr_t value); * one. The runtime maintains this for xco_suspend. */ xco_t *xco_self(void); -/* Resume c, delivering value as the result of its pending xco_suspend - * (or as fn's argument, on the first resume). Returns when c suspends - * or returns. Resuming a coroutine that is not XSTEP_INIT or - * XSTEP_SUSPENDED is undefined. Equivalent to xstep(&c->base, value). */ -static inline xstep_result_t xco_resume(xco_t *c, uintptr_t value) { - return xstep(&c->base, value); -} - -/* Read status without resuming. */ -static inline xstep_status_t xco_status(const xco_t *c) { - return xstep_status(&c->base); -} +/* Resuming a coroutine is just driving its xstep: callers use + * xstep(&c->base, v) directly. Reading status without resuming is + * xstep_status(&c->base). The xco layer adds no separate vocabulary + * for these — that's the unification with hand-coded state machines. + * Resuming a coroutine that is not XSTEP_INIT or XSTEP_SUSPENDED is + * undefined. */ -/* Convenience: init then resume in one call. */ +/* Convenience: init then first-step in one call. */ static inline xstep_result_t xco_spawn(xco_t *c, xco_fn fn, void *stack_base, size_t stack_len, uintptr_t arg) { xco_init(c, fn, stack_base, stack_len); - return xco_resume(c, arg); + return xstep(&c->base, arg); } /* Cooperative yield. Enqueues self on rt's ready queue and suspends; @@ -154,24 +148,18 @@ typedef struct xco_task { } xco_task_t; /* Initialize xt to run fn on the given stack. After this the embedded - * xco is XSTEP_INIT; drive it with xco_task_resume / xco_task_spawn. */ + * xco is XSTEP_INIT; drive it with xstep(&xt->co.base, v) or use + * xco_task_spawn for the init-and-first-step convenience. */ void xco_task_init(xco_task_t *xt, xco_task_fn fn, void *stack_base, size_t stack_len); -/* Resume the task. value is delivered as fn's arg on the first call and - * as the result of the pending xco_suspend on subsequent calls — same - * semantics as xco_resume on a bare xco. */ -static inline xstep_result_t xco_task_resume(xco_task_t *xt, uintptr_t value) { - return xco_resume(&xt->co, value); -} - -/* Convenience: init then first-resume in one call. arg is delivered as +/* Convenience: init then first-step in one call. arg is delivered as * fn's argument. */ static inline xstep_result_t xco_task_spawn(xco_task_t *xt, xco_task_fn fn, void *stack_base, size_t stack_len, uintptr_t arg) { xco_task_init(xt, fn, stack_base, stack_len); - return xco_task_resume(xt, arg); + return xstep(&xt->co.base, arg); } #endif /* XCO_H */