xco

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

commit a5b1bc4c164eec894ea9b6ca5f9de807da6b7988
parent 1d21190e6bf4de8704273bef5bc4863f5010bd6d
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue,  5 May 2026 01:24:42 -0700

Add event substrate: waker, latch, select, runtime

A pollable event abstraction on top of xstep. Three layers:

  waker_t    Intrusive notification node, doubly-linked while parked
             on an event waitlist (O(1) unpark) and singly-linked on
             the runtime ready queue. fire(w, value) delivers the
             event's payload directly.

  event_t    Abstract pollable: try / park / unpark. Concrete impls
             provided: latch_t (one-shot sticky bool with payload)
             and select_event_t (any-of N events, composes since a
             select is itself an event).

  runtime_t  Intrusive FIFO of step-wakers. rt_run drains it,
             resuming each xstep with the value its waker captured.

Caller-provided storage throughout — no allocation, no atomics,
single-threaded. The one-waker invariant (an xstep is parked on at
most one event) is what lets a single step_waker live inline in the
xstep and a single next/prev pair serve both list memberships;
multi-wait composes through select rather than per-step bookkeeping.

Diffstat:
MMakefile | 12++++++------
Aevent.c | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aevent.h | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/test_event.c | 317+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 699 insertions(+), 6 deletions(-)

diff --git a/Makefile b/Makefile @@ -16,12 +16,12 @@ BUILD := build CPPFLAGS += -I. -I$(ARCHDIR) -SRCS := xco.c $(ARCHDIR)/xco_arch.c +SRCS := xco.c event.c $(ARCHDIR)/xco_arch.c OBJS := $(SRCS:%.c=$(BUILD)/%.o) LIB := $(BUILD)/libxco.a -TEST_SRC := tests/test_xco.c -TEST_BIN := $(BUILD)/test_xco +TEST_SRCS := tests/test_xco.c tests/test_event.c +TEST_BINS := $(TEST_SRCS:tests/%.c=$(BUILD)/%) all: $(LIB) @@ -32,12 +32,12 @@ $(BUILD)/%.o: %.c @mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< -$(TEST_BIN): $(TEST_SRC) $(LIB) +$(BUILD)/test_%: tests/test_%.c $(LIB) @mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< $(LIB) -test: $(TEST_BIN) - $(TEST_BIN) +test: $(TEST_BINS) + @for t in $(TEST_BINS); do echo "==> $$t"; $$t || exit 1; done clean: rm -rf $(BUILD) diff --git a/event.c b/event.c @@ -0,0 +1,165 @@ +/* + * event.c — implementations for event.h. + * + * Each event type is a small struct with a static vtable. Waitlists + * are singly-linked LIFO (push at head, pop at head); fire-all detaches + * the whole list before iterating so callbacks can do anything, + * including re-park or unpark sibling wakers, without iterator hazards. + */ + +#include "event.h" + +#include <assert.h> +#include <stddef.h> + +/* ---- Runtime ---------------------------------------------------------- */ + +/* rt_init and rt_enqueue are defined inline in event.h. */ + +static waker_t *rt_dequeue(runtime_t *rt) { + waker_t *w = rt->head; + if (!w) return NULL; + rt->head = w->next; + if (!rt->head) rt->tail = NULL; + w->next = NULL; + return w; +} + +void rt_run(runtime_t *rt) { + /* The runtime ready queue holds only step-wakers. Other waker + * shapes (e.g. select_input) fire synchronously inside event + * notify paths and never reach here. */ + for (waker_t *w; (w = rt_dequeue(rt));) { + step_waker_t *sw = (step_waker_t *)w; + xstep(sw->step, sw->resume_value); + } +} + +/* ---- Step waker ------------------------------------------------------- */ + +/* Exposed (with leading underscore) so the inline step_waker_init in + * event.h can install it without dragging the body into the header. */ +void _step_waker_fire(waker_t *w, uintptr_t value) { + step_waker_t *sw = (step_waker_t *)w; + sw->resume_value = value; + rt_enqueue(sw->rt, w); +} + +/* ---- Latch ------------------------------------------------------------ */ + +static bool latch_try(event_t *e, uintptr_t *out) { + latch_t *l = (latch_t *)e; + if (!l->set) return false; + if (out) *out = l->value; + return true; +} + +static void latch_park(event_t *e, waker_t *w) { + latch_t *l = (latch_t *)e; + /* Single-threaded contract: caller must have just observed try=false. */ + assert(!l->set); + /* One-waker invariant: w must arrive clean. Detach paths (latch_set + * iterator, latch_unpark, select_event_deinit) all leave wakers in + * this state. A trip here means a double-park bug. */ + assert(!w->prev && !w->next); + w->next = l->waiters; + if (l->waiters) l->waiters->prev = w; + l->waiters = w; +} + +static void latch_unpark(event_t *e, waker_t *w) { + latch_t *l = (latch_t *)e; + /* Detect "not on this list" via the invariant maintained by park + * and the detach paths: a parked waker has prev set OR is the + * head; a detached one has prev == NULL and is not the head. */ + if (!w->prev && l->waiters != w) return; + if (w->prev) w->prev->next = w->next; + else l->waiters = w->next; + if (w->next) w->next->prev = w->prev; + w->prev = w->next = NULL; +} + +/* Exposed so the inline latch_init in event.h can reference it. */ +const event_vtable_t _latch_vt = { + .try_ = latch_try, + .park = latch_park, + .unpark = latch_unpark, +}; + +void latch_set(latch_t *l, uintptr_t value) { + if (l->set) return; + l->set = true; + l->value = value; + + /* Detach the whole waitlist before firing. A waker's fire callback + * might do anything (including unpark a sibling on another event), + * but it cannot mutate this list — it's already gone. */ + waker_t *w = l->waiters; + l->waiters = NULL; + while (w) { + waker_t *next = w->next; + w->next = NULL; + w->prev = NULL; + w->fire(w, value); + w = next; + } +} + +/* ---- Select ----------------------------------------------------------- */ + +static void select_input_fire(waker_t *w, uintptr_t value) { + /* The input's own value is irrelevant here — select delivers the + * winning index. Consumers re-try the input event for its payload. */ + (void)value; + select_input_t *in = (select_input_t *)w; + select_event_t *s = in->parent; + size_t i = (size_t)(in - s->inputs); + + /* Sticky: only the first input to fire records its index. Later + * stragglers (if any escape unparking) hit the idempotent guard. */ + if (s->done.set) return; + latch_set(&s->done, i); + + /* Disarm everyone else so their wakers don't dangle on input + * waitlists past the select's lifetime. */ + for (size_t j = 0; j < s->n; j++) { + if (j != i) event_unpark(s->inputs[j].src, &s->inputs[j].w); + } +} + +void select_event_init(select_event_t *s, + select_input_t *inputs, size_t n, + event_t *const *srcs) { + latch_init(&s->done); + s->inputs = inputs; + s->n = n; + + /* Fast path: an input already ready. Fire and skip parking entirely + * so deinit has nothing to disarm. */ + for (size_t i = 0; i < n; i++) { + uintptr_t v; + if (event_try(srcs[i], &v)) { + latch_set(&s->done, i); + return; + } + } + + for (size_t i = 0; i < n; i++) { + inputs[i].w.next = NULL; + inputs[i].w.prev = NULL; + inputs[i].w.fire = select_input_fire; + inputs[i].src = srcs[i]; + inputs[i].parent = s; + event_park(srcs[i], &inputs[i].w); + } +} + +void select_event_deinit(select_event_t *s) { + /* If the select fired, survivors were already disarmed by + * select_input_fire. If the fast path fired, nothing was ever + * parked. Either way, set => nothing to do. */ + if (s->done.set) return; + for (size_t i = 0; i < s->n; i++) { + event_unpark(s->inputs[i].src, &s->inputs[i].w); + } +} diff --git a/event.h b/event.h @@ -0,0 +1,211 @@ +/* + * event.h — minimal pollable event substrate. C11. + * + * Three layers, built on xstep: + * + * waker_t Intrusive notification node. Holds a fire callback and a + * single next-pointer reused across event waitlists and the + * runtime's ready queue (a waker is on at most one list at + * a time). + * + * event_t Abstract pollable: try / park / unpark. Concrete events + * (latch, select, channel, timer, ...) embed event_t as + * their first member and supply a vtable. Every blocking + * primitive in this codebase is just an event. + * + * runtime_t Intrusive FIFO of step-wakers. rt_run drains it, + * resuming each xstep until none are ready. + * + * Concrete events provided here: + * latch_t One-shot sticky bool with optional payload. + * select_event_t Fires when any of N input events fires; carries + * the index of the winner. Composes (a select is + * itself an event_t). + * + * All storage is caller-provided. No allocation, no atomics. + * + * Single-threaded only — matches xco's thread affinity. The contract + * "try first, park only if try failed" is race-free because nothing + * else runs between the two calls. + * + * One-waker invariant. An xstep, while suspended, is parked on at + * most one event. Multi-wait is composed in the event graph: build a + * select_event (or any future combinator — all-of, timeout, ...) and + * park on that. The xstep never sees more than one event directly. + * This is what lets a single step_waker_t live inline in the xstep + * and a single next/prev pair serve both event waitlists and the + * runtime ready queue (the two list memberships are disjoint in time). + * + * Standard usage from a state machine: + * + * uintptr_t v; + * if (event_try(e, &v)) { ... use v ... } + * else { event_park(e, &my_waker.base); return SUSPENDED; } + * + * Standard usage from an xco coroutine wrapper: + * + * uintptr_t v; + * if (!event_try(e, &v)) { + * step_waker_t sw; + * step_waker_init(&sw, rt, &xco_self()->base); + * event_park(e, &sw.base); + * xco_suspend(0); + * (void)event_try(e, &v); // now ready + * } + */ + +#ifndef EVENT_H +#define EVENT_H + +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +#include "xstep.h" + +/* ---- Waker ------------------------------------------------------------ */ + +typedef struct waker waker_t; +struct waker { + /* Doubly-linked while parked on an event waitlist, so unpark is + * O(1). Reused as the singly-linked next pointer while on the + * runtime ready queue (FIFO, no removal from middle); prev is + * undefined in that state and reset on the next park. */ + waker_t *next; + waker_t *prev; + /* Fire callback. value is the event's payload at fire time — sticky + * events also store it on themselves, transient events (channels, + * one-shot signals) deliver only here. Wakers that don't care + * about the value just ignore the parameter. */ + void (*fire)(waker_t *w, uintptr_t value); +}; + +/* ---- Event ------------------------------------------------------------ */ + +typedef struct event event_t; + +typedef struct { + /* Inline-succeed if possible. *out receives the event's value when + * it has one (latch payload, select winner index, channel data). + * out may be NULL if the caller doesn't need the value. */ + bool (*try_)(event_t *e, uintptr_t *out); + /* Arm w on the event's waitlist. Caller's contract: try_ returned + * false. Single-threaded runtime guarantees no transition between. */ + void (*park)(event_t *e, waker_t *w); + /* Remove w from the waitlist. Idempotent: no-op if not parked. */ + void (*unpark)(event_t *e, waker_t *w); +} event_vtable_t; + +struct event { const event_vtable_t *vt; }; + +static inline bool event_try(event_t *e, uintptr_t *out) { + return e->vt->try_(e, out); +} +static inline void event_park(event_t *e, waker_t *w) { e->vt->park(e, w); } +static inline void event_unpark(event_t *e, waker_t *w) { e->vt->unpark(e, w); } + +/* ---- Runtime ---------------------------------------------------------- */ + +typedef struct runtime { + waker_t *head, *tail; +} runtime_t; + +static inline void rt_init(runtime_t *rt) { rt->head = rt->tail = NULL; } + +/* Append w to the ready queue. Used by step_waker_fire and by anyone + * else that wants a waker resumed by the scheduler. */ +static inline void rt_enqueue(runtime_t *rt, waker_t *w) { + w->next = NULL; + if (rt->tail) rt->tail->next = w; + else rt->head = w; + rt->tail = w; +} + +/* Drain the ready queue, resuming each step-waker's xstep, until empty. + * Steps may re-arm on events (and thus leave the queue) or enqueue + * other steps; rt_run keeps going until quiescent. */ +void rt_run(runtime_t *rt); + +/* The canonical bridge between events and the scheduler. When fired, + * stashes the value and enqueues itself onto rt; rt_run pops it and + * calls xstep(step, value), so the resumed step receives the event's + * payload directly without a re-try. */ +typedef struct { + waker_t base; + runtime_t *rt; + xstep_t *step; + uintptr_t resume_value; /* set by fire, consumed by rt_run */ +} step_waker_t; + +/* Defined in event.c; declared here so step_waker_init can install it. */ +void _step_waker_fire(waker_t *w, uintptr_t value); + +static inline void step_waker_init(step_waker_t *sw, runtime_t *rt, xstep_t *s) { + sw->base.next = NULL; + sw->base.prev = NULL; + sw->base.fire = _step_waker_fire; + sw->rt = rt; + sw->step = s; + sw->resume_value = 0; +} + +/* ---- Latch ------------------------------------------------------------ */ + +/* One-shot sticky event. set() flips the bit, stores the payload, and + * fires every waiter. Subsequent set() calls are ignored. To re-arm, + * reinitialize a fresh latch. */ +typedef struct { + event_t base; + bool set; + uintptr_t value; + waker_t *waiters; +} latch_t; + +/* Defined in event.c; referenced by latch_init. */ +extern const event_vtable_t _latch_vt; + +static inline void latch_init(latch_t *l) { + l->base.vt = &_latch_vt; + l->set = false; + l->value = 0; + l->waiters = NULL; +} + +void latch_set(latch_t *l, uintptr_t value); + +/* ---- Select ----------------------------------------------------------- */ + +/* Fires when any of N input events fires. The winning index is the + * payload of the underlying latch; consumers re-try that input to pull + * its actual value. Composes: a select_event is itself an event. */ + +typedef struct select_event select_event_t; + +/* Per-input arming record. Caller-allocated as an array of n alongside + * the select_event. Internal layout — fields are touched only by + * select_event_init / select_input_fire. */ +typedef struct { + waker_t w; + event_t *src; + select_event_t *parent; +} select_input_t; + +struct select_event { + latch_t done; /* fired with the winner's index */ + select_input_t *inputs; + size_t n; +}; + +/* Initialize. inputs[] is caller-provided storage for n nodes; srcs[] + * is the array of n input event pointers (read once during init). + * If any input is already ready, the select fires immediately and no + * wakers are parked. Use &s->done.base as the resulting event_t. */ +void select_event_init(select_event_t *s, + select_input_t *inputs, size_t n, + event_t *const *srcs); + +/* Disarm any inputs still parked. Safe to call after fire (no-op). + * Required before s leaves scope if it has not yet fired. */ +void select_event_deinit(select_event_t *s); + +#endif /* EVENT_H */ diff --git a/tests/test_event.c b/tests/test_event.c @@ -0,0 +1,317 @@ +/* + * test_event.c — exercises latch_t, select_event_t, runtime_t. + * + * Drives hand-coded xstep state machines through the event substrate + * — no coroutines required. A "waiter" is the canonical pattern: try + * the event, park if not ready, re-try after wake. + */ + +#include "event.h" +#include "xstep.h" + +#include <assert.h> +#include <stdio.h> + +/* ---- Waiter: blocks on one event, captures its value -------------- */ + +typedef struct { + xstep_t base; + event_t *e; + runtime_t *rt; + step_waker_t sw; + int phase; + uintptr_t got; +} waiter_t; + +static xstep_result_t waiter_step(xstep_t *s, uintptr_t v) { + waiter_t *w = (waiter_t *)s; + switch (w->phase) { + case 0: { + uintptr_t out; + if (event_try(w->e, &out)) { + w->got = out; + w->phase = 2; + return (xstep_result_t){out, XSTEP_DEAD}; + } + step_waker_init(&w->sw, w->rt, &w->base); + event_park(w->e, &w->sw.base); + w->phase = 1; + return (xstep_result_t){0, XSTEP_SUSPENDED}; + } + case 1: + /* v is the value handed in by the fire site (latch payload, or + * for select, the winning index). No re-try needed. */ + w->got = v; + w->phase = 2; + return (xstep_result_t){v, XSTEP_DEAD}; + } + __builtin_unreachable(); +} + +static void waiter_init(waiter_t *w, runtime_t *rt, event_t *e) { + w->base = (xstep_t){.step = waiter_step, .status = XSTEP_INIT}; + w->e = e; + w->rt = rt; + w->phase = 0; + w->got = 0; +} + +/* ---- Latch tests --------------------------------------------------- */ + +static void test_latch_wake(void) { + runtime_t rt; rt_init(&rt); + latch_t l; latch_init(&l); + + waiter_t w; waiter_init(&w, &rt, &l.base); + + xstep_result_t r = xstep(&w.base, 0); + assert(r.status == XSTEP_SUSPENDED); + assert(rt.head == NULL); /* parked, not queued */ + assert(l.waiters == &w.sw.base); /* armed on the latch */ + + latch_set(&l, 42); + assert(l.waiters == NULL); /* drained on fire */ + assert(rt.head != NULL); /* fire enqueued the step */ + + rt_run(&rt); + assert(xstep_status(&w.base) == XSTEP_DEAD); + assert(w.got == 42); + assert(rt.head == NULL); +} + +static void test_latch_already_set(void) { + /* Already-set latch: try succeeds inline, no park, no queueing. */ + runtime_t rt; rt_init(&rt); + latch_t l; latch_init(&l); + latch_set(&l, 7); + + waiter_t w; waiter_init(&w, &rt, &l.base); + + xstep_result_t r = xstep(&w.base, 0); + assert(r.status == XSTEP_DEAD); + assert(r.value == 7); + assert(w.got == 7); + assert(rt.head == NULL); +} + +static void test_latch_multi_waiter(void) { + /* One latch_set wakes every waiter. */ + runtime_t rt; rt_init(&rt); + latch_t l; latch_init(&l); + + waiter_t a, b, c; + waiter_init(&a, &rt, &l.base); + waiter_init(&b, &rt, &l.base); + waiter_init(&c, &rt, &l.base); + + xstep(&a.base, 0); + xstep(&b.base, 0); + xstep(&c.base, 0); + assert(xstep_status(&a.base) == XSTEP_SUSPENDED); + assert(xstep_status(&b.base) == XSTEP_SUSPENDED); + assert(xstep_status(&c.base) == XSTEP_SUSPENDED); + + latch_set(&l, 99); + rt_run(&rt); + + assert(xstep_status(&a.base) == XSTEP_DEAD && a.got == 99); + assert(xstep_status(&b.base) == XSTEP_DEAD && b.got == 99); + assert(xstep_status(&c.base) == XSTEP_DEAD && c.got == 99); +} + +static void test_latch_set_idempotent(void) { + latch_t l; latch_init(&l); + latch_set(&l, 1); + latch_set(&l, 2); /* ignored */ + + uintptr_t v = 0; + assert(event_try(&l.base, &v)); + assert(v == 1); +} + +static void test_latch_unpark(void) { + /* Manually park then unpark — verify the waker is removed cleanly. */ + runtime_t rt; rt_init(&rt); + latch_t l; latch_init(&l); + + step_waker_t sw1, sw2, sw3; + /* Step pointer is just a sentinel here; we never run them. */ + step_waker_init(&sw1, &rt, (xstep_t *)0x1); + step_waker_init(&sw2, &rt, (xstep_t *)0x2); + step_waker_init(&sw3, &rt, (xstep_t *)0x3); + event_park(&l.base, &sw1.base); + event_park(&l.base, &sw2.base); + event_park(&l.base, &sw3.base); + + /* Remove the middle one. */ + event_unpark(&l.base, &sw2.base); + assert(sw2.base.next == NULL); + + /* Unpark of an already-removed waker is a no-op. */ + event_unpark(&l.base, &sw2.base); + + /* sw1 and sw3 still on the list. */ + int seen1 = 0, seen3 = 0; + for (waker_t *w = l.waiters; w; w = w->next) { + if (w == &sw1.base) seen1 = 1; + if (w == &sw3.base) seen3 = 1; + assert(w != &sw2.base); + } + assert(seen1 && seen3); + + /* Drain so we don't leave dangling armed wakers. */ + event_unpark(&l.base, &sw1.base); + event_unpark(&l.base, &sw3.base); + assert(l.waiters == NULL); +} + +/* ---- Select tests -------------------------------------------------- */ + +static void test_select_winner(void) { + runtime_t rt; rt_init(&rt); + latch_t a, b, c; + latch_init(&a); latch_init(&b); latch_init(&c); + + select_event_t s; + select_input_t inputs[3]; + event_t *srcs[3] = {&a.base, &b.base, &c.base}; + select_event_init(&s, inputs, 3, srcs); + + waiter_t w; waiter_init(&w, &rt, &s.done.base); + xstep(&w.base, 0); + assert(xstep_status(&w.base) == XSTEP_SUSPENDED); + assert(a.waiters && b.waiters && c.waiters); /* all armed */ + + latch_set(&b, 0xBBB); + rt_run(&rt); + + assert(xstep_status(&w.base) == XSTEP_DEAD); + assert(w.got == 1); /* b's index */ + + /* Losers were disarmed by select_input_fire. */ + assert(a.waiters == NULL); + assert(c.waiters == NULL); + + /* Re-trying the winning input yields its actual payload. */ + uintptr_t v; + assert(event_try(&b.base, &v)); + assert(v == 0xBBB); + + select_event_deinit(&s); +} + +static void test_select_fast_path(void) { + /* Input already set at init: fire immediately, never park anyone. */ + latch_t a, b; + latch_init(&a); latch_init(&b); + latch_set(&a, 0xAAA); + + select_event_t s; + select_input_t inputs[2]; + event_t *srcs[2] = {&a.base, &b.base}; + select_event_init(&s, inputs, 2, srcs); + + uintptr_t v; + assert(event_try(&s.done.base, &v)); + assert(v == 0); /* a wins */ + assert(b.waiters == NULL); /* nothing parked on the loser */ + + select_event_deinit(&s); +} + +static void test_select_deinit_unparks(void) { + /* Selecting then dropping (no input fires) must release input wakers. */ + latch_t a, b; + latch_init(&a); latch_init(&b); + + select_event_t s; + select_input_t inputs[2]; + event_t *srcs[2] = {&a.base, &b.base}; + select_event_init(&s, inputs, 2, srcs); + assert(a.waiters != NULL); + assert(b.waiters != NULL); + + select_event_deinit(&s); + assert(a.waiters == NULL); + assert(b.waiters == NULL); +} + +static void test_select_compose(void) { + /* select(select(A, B), C): firing A propagates through both layers. */ + runtime_t rt; rt_init(&rt); + latch_t a, b, c; + latch_init(&a); latch_init(&b); latch_init(&c); + + select_event_t inner; + select_input_t in_inputs[2]; + event_t *in_srcs[2] = {&a.base, &b.base}; + select_event_init(&inner, in_inputs, 2, in_srcs); + + select_event_t outer; + select_input_t out_inputs[2]; + event_t *out_srcs[2] = {&inner.done.base, &c.base}; + select_event_init(&outer, out_inputs, 2, out_srcs); + + waiter_t w; waiter_init(&w, &rt, &outer.done.base); + xstep(&w.base, 0); + assert(xstep_status(&w.base) == XSTEP_SUSPENDED); + + latch_set(&a, 0); + rt_run(&rt); + + assert(xstep_status(&w.base) == XSTEP_DEAD); + assert(w.got == 0); /* outer winner: inner (index 0) */ + + uintptr_t v; + assert(event_try(&inner.done.base, &v)); + assert(v == 0); /* inner winner: a (index 0) */ + + /* The unrelated source c was disarmed when outer fired. */ + assert(c.waiters == NULL); + /* And b was disarmed when inner fired. */ + assert(b.waiters == NULL); + + select_event_deinit(&outer); + select_event_deinit(&inner); +} + +/* ---- Runtime test -------------------------------------------------- */ + +static void test_runtime_drains(void) { + /* rt_run keeps going until quiescent — including steps queued + * during another step's resumption. Three waiters on one latch + * all reach DEAD in a single rt_run. */ + runtime_t rt; rt_init(&rt); + latch_t l; latch_init(&l); + + waiter_t w[3]; + for (int i = 0; i < 3; i++) waiter_init(&w[i], &rt, &l.base); + for (int i = 0; i < 3; i++) xstep(&w[i].base, 0); + + latch_set(&l, 0xC0DE); + rt_run(&rt); + + assert(rt.head == NULL); + for (int i = 0; i < 3; i++) { + assert(xstep_status(&w[i].base) == XSTEP_DEAD); + assert(w[i].got == 0xC0DE); + } +} + +int main(void) { + test_latch_wake(); + test_latch_already_set(); + test_latch_multi_waiter(); + test_latch_set_idempotent(); + test_latch_unpark(); + + test_select_winner(); + test_select_fast_path(); + test_select_deinit_unparks(); + test_select_compose(); + + test_runtime_drains(); + + printf("ok\n"); + return 0; +}