commit 1d21190e6bf4de8704273bef5bc4863f5010bd6d
parent 552dff0b3b6d727f42c428080c897229f31369d7
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 4 May 2026 21:50:42 -0700
Factor resumable-function interface into xstep.h
Coroutines and hand-coded state machines are both resumable functions
differing only in mechanism. Pull the shared shape — step function,
status, result — into xstep_t so generic code can drive either kind
through one interface.
xco_t embeds xstep_t as its first member and registers an internal
xco_step into base.step; xco_resume / xco_status become typed inline
forwards to xstep() / xstep_status(). Status enum and result type
unify under XSTEP_*; the test now drives both a coroutine and a
hand-coded state machine through the same drive_counter(xstep_t *).
Diffstat:
| M | tests/test_xco.c | | | 79 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ |
| M | xco.c | | | 51 | +++++++++++++++++++++++++++------------------------ |
| M | xco.h | | | 61 | +++++++++++++++++++++++++++++++------------------------------ |
| A | xstep.h | | | 71 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 196 insertions(+), 66 deletions(-)
diff --git a/tests/test_xco.c b/tests/test_xco.c
@@ -2,7 +2,9 @@
* test_xco.c — smoke test for the xco API.
*
* Exercises: status transitions, value channel in both directions,
- * multiple suspends, nested resumes, xco_self.
+ * multiple suspends, nested resumes, xco_self. Also drives a hand-
+ * coded xstep_t state machine through the same generic interface as
+ * a coroutine, to verify the unification.
*/
#include "xco.h"
@@ -16,6 +18,7 @@
static alignas(XCO_STACK_ALIGN) unsigned char stack_a[STACK_BYTES];
static alignas(XCO_STACK_ALIGN) unsigned char stack_b[STACK_BYTES];
+static alignas(XCO_STACK_ALIGN) unsigned char stack_c[STACK_BYTES];
/* Receives n via the first resume, then yields n+1, n+2, n+3, returns n+4. */
static uintptr_t counter(uintptr_t n) {
@@ -29,23 +32,64 @@ static uintptr_t counter(uintptr_t n) {
return n + 4;
}
+/* Hand-coded state-machine implementation of the same contract:
+ * first input n, then yield n+1, n+2, n+3, return n+4. No stack
+ * switch — suspension is just returning XSTEP_SUSPENDED. */
+typedef struct {
+ xstep_t base;
+ int phase;
+ uintptr_t n;
+} counter_sm_t;
+
+static xstep_result_t counter_sm_step(xstep_t *s, uintptr_t v) {
+ counter_sm_t *p = (counter_sm_t *)s;
+ switch (p->phase++) {
+ case 0: p->n = v; return (xstep_result_t){v + 1, XSTEP_SUSPENDED};
+ case 1: return (xstep_result_t){p->n + 2, XSTEP_SUSPENDED};
+ case 2: return (xstep_result_t){p->n + 3, XSTEP_SUSPENDED};
+ case 3: return (xstep_result_t){p->n + 4, XSTEP_DEAD};
+ }
+ __builtin_unreachable();
+}
+
+static void counter_sm_init(counter_sm_t *p) {
+ p->base = (xstep_t){.step = counter_sm_step, .status = XSTEP_INIT};
+ p->phase = 0;
+ p->n = 0;
+}
+
+/* Generic driver: takes any xstep_t implementing the counter contract
+ * and walks it to completion. Knows nothing about coroutines. */
+static void drive_counter(xstep_t *s) {
+ assert(xstep_status(s) == XSTEP_INIT);
+ xstep_result_t r = xstep(s, 10);
+ assert(r.status == XSTEP_SUSPENDED && r.value == 11);
+ r = xstep(s, 100);
+ assert(r.status == XSTEP_SUSPENDED && r.value == 12);
+ r = xstep(s, 200);
+ assert(r.status == XSTEP_SUSPENDED && r.value == 13);
+ r = xstep(s, 300);
+ assert(r.status == XSTEP_DEAD && r.value == 14);
+ assert(xstep_status(s) == XSTEP_DEAD);
+}
+
static xco_t inner;
/* Spawns `inner` and pumps it once, demonstrating nested resumes. */
static uintptr_t outer(uintptr_t arg) {
(void)arg;
- xco_result_t r = xco_spawn(&inner, counter, stack_b, STACK_BYTES, 10);
- assert(r.status == XCO_SUSPENDED && r.value == 11);
+ xstep_result_t r = xco_spawn(&inner, counter, stack_b, STACK_BYTES, 10);
+ assert(r.status == XSTEP_SUSPENDED && r.value == 11);
/* 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);
- assert(r.status == XCO_SUSPENDED && r.value == 12);
+ assert(r.status == XSTEP_SUSPENDED && r.value == 12);
r = xco_resume(&inner, 200);
- assert(r.status == XCO_SUSPENDED && r.value == 13);
+ assert(r.status == XSTEP_SUSPENDED && r.value == 13);
r = xco_resume(&inner, 300);
- assert(r.status == XCO_DEAD && r.value == 14);
+ assert(r.status == XSTEP_DEAD && r.value == 14);
return 999;
}
@@ -54,18 +98,29 @@ int main(void) {
xco_t c;
xco_init(&c, outer, stack_a, STACK_BYTES);
- assert(xco_status(&c) == XCO_INIT);
+ assert(xco_status(&c) == XSTEP_INIT);
- xco_result_t r = xco_resume(&c, 0);
+ xstep_result_t r = xco_resume(&c, 0);
assert(xco_self() == NULL);
- assert(r.status == XCO_SUSPENDED);
+ assert(r.status == XSTEP_SUSPENDED);
assert(r.value == 11);
- assert(xco_status(&c) == XCO_SUSPENDED);
+ assert(xco_status(&c) == XSTEP_SUSPENDED);
r = xco_resume(&c, 42);
- assert(r.status == XCO_DEAD);
+ assert(r.status == XSTEP_DEAD);
assert(r.value == 999);
- assert(xco_status(&c) == XCO_DEAD);
+ assert(xco_status(&c) == XSTEP_DEAD);
+
+ /* Unification: the same generic driver pumps a coroutine and a
+ * hand-coded state machine, both reaching XSTEP_DEAD with the
+ * expected value sequence. */
+ xco_t co;
+ xco_init(&co, counter, stack_c, STACK_BYTES);
+ drive_counter(&co.base);
+
+ counter_sm_t sm;
+ counter_sm_init(&sm);
+ drive_counter(&sm.base);
printf("ok\n");
return 0;
diff --git a/xco.c b/xco.c
@@ -7,6 +7,10 @@
* resume chain, the trampoline wrapping user fn entry/exit, and the
* 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().
+ *
* This translation unit is architecture-neutral. The platform context
* type is forward-declared (in xco_platform.h) and only ever referred
* to by pointer here, so its actual size and layout never cross into
@@ -23,14 +27,14 @@
#include <stdint.h>
typedef struct xco_impl {
+ xstep_t base; /* must be first; aliases xco_t.base */
_Alignas(_XCO_CTX_ALIGN) unsigned char ctx_buf[_XCO_CTX_SIZE];
- xco_status_t status;
- xco_platform_ctx_t *resumer_ctx; /* where suspend/return goes */
+ xco_platform_ctx_t *resumer_ctx; /* where suspend/return goes */
xco_fn fn;
} xco_impl_t;
-_Static_assert(sizeof(xco_impl_t) <= XCO_SIZE, "XCO_SIZE too small");
-_Static_assert(_Alignof(xco_impl_t) <= XCO_ALIGN, "XCO_ALIGN too small");
+_Static_assert(sizeof(xco_impl_t) <= sizeof(xco_t), "xco_t too small");
+_Static_assert(_Alignof(xco_impl_t) <= _Alignof(xco_t), "xco_t under-aligned");
static inline xco_impl_t *impl(xco_t *c) { return (xco_impl_t *)c; }
static inline xco_platform_ctx_t *ctx_of(xco_impl_t *ci) {
@@ -54,27 +58,20 @@ static void trampoline(uintptr_t arg) {
xco_impl_t *self = t_current;
uintptr_t ret = self->fn(arg);
- self->status = XCO_DEAD;
+ self->base.status = XSTEP_DEAD;
(void)xco_platform_switch(ctx_of(self), self->resumer_ctx, ret);
__builtin_unreachable();
}
-void xco_init(xco_t *c, xco_fn fn,
- void *stack_base, size_t stack_len) {
- xco_impl_t *ci = impl(c);
- ci->status = XCO_INIT;
- ci->fn = fn;
- ci->resumer_ctx = NULL;
- xco_platform_init(ctx_of(ci), stack_base, stack_len, trampoline);
-}
-
-xco_result_t xco_resume(xco_t *c, uintptr_t value) {
- xco_impl_t *next = impl(c);
- assert(next->status == XCO_INIT || next->status == XCO_SUSPENDED);
+/* xstep_fn entry point wired into base.step at init time. Generic
+ * xstep callers route through here; xco_resume is a typed forward. */
+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);
xco_impl_t *prev = t_current;
next->resumer_ctx = prev ? ctx_of(prev) : main_ctx();
- next->status = XCO_RUNNING;
+ next->base.status = XSTEP_RUNNING;
t_current = next;
uintptr_t back = xco_platform_switch(next->resumer_ctx,
@@ -83,20 +80,26 @@ xco_result_t xco_resume(xco_t *c, uintptr_t value) {
/* Coroutine has either suspended or returned; status is already
* set correctly by xco_suspend or by the trampoline. */
t_current = prev;
- return (xco_result_t){ .value = back, .status = next->status };
+ return (xstep_result_t){ .value = back, .status = next->base.status };
+}
+
+void xco_init(xco_t *c, xco_fn fn,
+ void *stack_base, size_t stack_len) {
+ xco_impl_t *ci = impl(c);
+ ci->base.step = xco_step;
+ ci->base.status = XSTEP_INIT;
+ ci->fn = fn;
+ ci->resumer_ctx = NULL;
+ xco_platform_init(ctx_of(ci), stack_base, stack_len, trampoline);
}
uintptr_t xco_suspend(uintptr_t value) {
xco_impl_t *self = t_current;
assert(self != NULL);
- self->status = XCO_SUSPENDED;
+ self->base.status = XSTEP_SUSPENDED;
return xco_platform_switch(ctx_of(self), self->resumer_ctx, value);
}
xco_t *xco_self(void) {
return (xco_t *)t_current;
}
-
-xco_status_t xco_status(const xco_t *c) {
- return ((const xco_impl_t *)c)->status;
-}
diff --git a/xco.h b/xco.h
@@ -1,9 +1,13 @@
/*
- * xco.h — minimal asymmetric coroutines. C11.
+ * xco.h — minimal asymmetric coroutines built atop xstep. C11.
*
* A coroutine is a (program counter, stack) pair that can be resumed
- * and suspended. Values pass between caller and coroutine through a
- * single uintptr_t channel; pack richer data behind a pointer.
+ * and suspended. xco_t embeds xstep_t as its first member, so a
+ * coroutine is one concrete kind of resumable function: generic code
+ * holding an xstep_t * works on coroutines and hand-coded state
+ * machines uniformly. Values pass between caller and coroutine
+ * through a single uintptr_t channel; pack richer data behind a
+ * pointer.
*
* Asymmetric: xco_suspend always returns to the most recent resumer.
* Resumes nest like function calls.
@@ -22,62 +26,59 @@
#include <stddef.h>
#include <stdint.h>
+#include "xstep.h"
/* Provides XCO_SIZE, XCO_ALIGN, XCO_STACK_ALIGN; resolved by the
* build to the arch-specific copy via the include path. */
#include "xco_arch.h"
-typedef enum {
- XCO_INIT, /* created, never resumed */
- XCO_RUNNING, /* executing, or in the active resume chain */
- XCO_SUSPENDED, /* yielded; resumable */
- XCO_DEAD, /* function returned */
-} xco_status_t;
-
/* 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 xco_resume result, with status XCO_DEAD. */
+ * the final xstep_result, with status XSTEP_DEAD. */
typedef uintptr_t (*xco_fn)(uintptr_t arg);
-typedef struct {
- uintptr_t value;
- xco_status_t status; /* XCO_SUSPENDED or XCO_DEAD after resume */
-} xco_result_t;
-
/* Coroutine control block. Allocate anywhere — on a stack, in a
- * struct, on the heap. Contents are private to the implementation. */
+ * struct, on the heap. xstep_t base is first so xco_t * can be passed
+ * directly to xstep() or any generic xstep_t * consumer. The trailing
+ * _private storage holds the saved register context and bookkeeping;
+ * its contents are private to the implementation. */
typedef struct xco {
+ xstep_t base;
_Alignas(XCO_ALIGN) unsigned char _private[XCO_SIZE];
} xco_t;
/* Initialize *c to run fn on [stack_base, stack_base + stack_len).
* stack_base must be XCO_STACK_ALIGN-aligned; the runtime picks the
* starting SP based on the architecture's stack growth direction.
- * Status after init is XCO_INIT. */
+ * Status after init is XSTEP_INIT. */
void xco_init(xco_t *c, xco_fn fn,
void *stack_base, size_t stack_len);
-/* 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 XCO_INIT or
- * XCO_SUSPENDED is undefined. */
-xco_result_t xco_resume(xco_t *c, uintptr_t value);
-
/* Suspend the currently running coroutine, returning value to its
- * resumer. Returns the value passed by the next xco_resume. Undefined
- * if called outside a coroutine. */
+ * resumer. Returns the value passed by the next resume. Undefined if
+ * called outside a coroutine. */
uintptr_t xco_suspend(uintptr_t value);
/* The currently running coroutine, or NULL if the caller is not in
* 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. */
-xco_status_t xco_status(const xco_t *c);
+static inline xstep_status_t xco_status(const xco_t *c) {
+ return xstep_status(&c->base);
+}
/* Convenience: init then resume in one call. */
-static inline xco_result_t xco_spawn(xco_t *c, xco_fn fn,
- void *stack_base, size_t stack_len,
- uintptr_t arg) {
+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);
}
diff --git a/xstep.h b/xstep.h
@@ -0,0 +1,71 @@
+/*
+ * xstep.h — generic resumable function interface. C11.
+ *
+ * An xstep_t is a value that can be driven forward one step at a
+ * time. Each step takes a uintptr_t in, returns one out, and reports
+ * whether the function suspended (more steps to come) or finished.
+ * Pack richer data behind a pointer.
+ *
+ * This is the substrate two implementations share: stack-switching
+ * coroutines (xco) and hand-coded state machines. The first member
+ * convention — embed xstep_t as the first field of your concrete
+ * type — lets a pointer to your type be passed wherever an xstep_t *
+ * is expected.
+ *
+ * typedef struct {
+ * xstep_t base;
+ * int phase;
+ * ...
+ * } parser_t;
+ *
+ * static xstep_result_t parser_step(xstep_t *s, uintptr_t v) {
+ * parser_t *p = (parser_t *)s;
+ * switch (p->phase) {
+ * case 0: p->phase = 1; return (xstep_result_t){v + 1, XSTEP_SUSPENDED};
+ * case 1: return (xstep_result_t){v * 2, XSTEP_DEAD};
+ * }
+ * __builtin_unreachable();
+ * }
+ *
+ * parser_t p = { .base = {.step = parser_step, .status = XSTEP_INIT} };
+ * xstep_result_t r = xstep(&p.base, 0);
+ */
+
+#ifndef XSTEP_H
+#define XSTEP_H
+
+#include <stdint.h>
+
+typedef enum {
+ XSTEP_INIT, /* created, never stepped */
+ XSTEP_RUNNING, /* inside step(), or in an active resume chain */
+ XSTEP_SUSPENDED, /* yielded; resumable */
+ XSTEP_DEAD, /* function returned */
+} xstep_status_t;
+
+typedef struct {
+ uintptr_t value;
+ xstep_status_t status;
+} xstep_result_t;
+
+typedef struct xstep xstep_t;
+typedef xstep_result_t (*xstep_fn)(xstep_t *s, uintptr_t value);
+
+struct xstep {
+ xstep_fn step;
+ xstep_status_t status; /* cached; xstep() syncs from each result */
+};
+
+/* Drive one step. The wrapper updates s->status from the returned
+ * result so step implementations only need to populate the result. */
+static inline xstep_result_t xstep(xstep_t *s, uintptr_t value) {
+ xstep_result_t r = s->step(s, value);
+ s->status = r.status;
+ return r;
+}
+
+static inline xstep_status_t xstep_status(const xstep_t *s) {
+ return s->status;
+}
+
+#endif /* XSTEP_H */