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:
| M | tests/test_xco.c | | | 43 | ++++++++++++++++++++++++++++++++----------- |
| M | xco.c | | | 15 | ++++++++------- |
| M | xco.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 */