Internals

← Promise

PromiseNode Hierarchy

All async computations implement PromiseNode<T> — a virtual interface with one method:

template <class T> class PromiseNode {
  using ValueType = typename FixVoid<T>::Type;
  virtual Option<ValueType> poll(const PromiseContext &cx) = 0;
};
  • Some(value) = ready, value extracted in the same call
  • None = pending, waker stored for later notification
  • One-shot: once Some is returned, poll() must never be called again

Node Types

Node TypePurposepoll() Behavior
ImmediatePromiseNode<T>Promise::resolve(v)Returns Some(v) — ignores waker
TransformPromiseNode<U, T, F>.then(fn)Polls dependency; if Some, applies fn
ChainPromiseNode<T>Auto-flatten Promise<Promise<T>>Polls outer; when ready, switches to inner
AdapterPromiseNode<T, Adapter>Generic adapter patternPolls ResolveState: check resolved → register waker → double-check
ManualResolveNode<T>async<T>() factorySame poll logic, no Adapter
YieldPromiseNodeyield()Returns Some(Void{})
AllTuplePromiseNode<Ts...>all() combinatorPolls all children, collects tuple when all done
AllVoidPromiseNode<N>all() all-voidCountdown, returns Some(Void{}) when all done
RacePromiseNode<T, N>race() combinatorReturns first Some, destroys losers

TransformPromiseNode

Four partial specializations handle the void-unit-type mapping: T→U, void→U, T→void, void→void. Uses _voidwrap::call / _voidwrap::call1 SFINAE helpers.

ChainPromiseNode

Uses m_inner != nullptr as a state machine: nullptr = polling outer, non-null = polling inner. No enum, no extra branch.

poll_state() — Shared Poll Logic

AdapterPromiseNode and ManualResolveNode share the same poll implementation via poll_state():

template <class T>
Option<T> poll_state(ResolveState<T> &s, const PromiseContext &cx) {
    if (s.resolved.load(std::memory_order_acquire))
        return std::move(s.value);           // Fast path
    s.waker.register_by_ref(cx.waker());     // May race with resolve
    if (s.resolved.load(std::memory_order_acquire)) {
        s.waker.wake();                      // Self-wake
        return std::move(s.value);
    }
    return none;
}

Void specialization returns Some(Void{}) when resolved (void resolve() doesn't set value).

ResolveState + PromiseResolver

struct ResolveState<T> {
  Option<T>          value;
  AtomicPromiseWaker waker;
  std::atomic<bool>  resolved{false};
};

PromiseResolver::resolve() — thread-safe, safe after node destruction:

void resolve(ValueType &&value) {
    auto s = m_weak.upgrade();               // ArcWeak → Option<Arc<State>>
    if (s.is_some()) {                       // Node still alive?
        auto &state = *s.unwrap();
        if (state.resolved.compare_exchange_strong(false, true, acq_rel)) {
            state.value = Option<T>(std::move(value));
            state.waker.wake();
        }
    }
    // else: node destroyed — silently drop
}

AtomicPromiseWaker — Lock-Free 2-Bit State Machine

Coordinates register_by_ref() (poll side) and wake() (resolve side) without a mutex:

StateBitsMeaning
WAITING00Idle
REGISTERING01poll side storing waker
WAKING10resolve side waking
RACE11Both collided — registerer self-wakes
register_by_ref(new_waker):
    CAS(00 → 01)
    ├─ success → store waker → exchange(01 → 00)
    │            ├─ prev == 00 → done
    │            └─ prev == 11 → self-wake
    └─ failure →
         ├─ prev == 10 → wake(new_waker)
         └─ prev == 11 → spin/CAS again

wake():
    fetch_or(10)
    ├─ prev == 00 → exclusive → wake stored waker → store(00)
    └─ prev == 01 → race → set WAKING bit
        └─ registerer's exchange sees 11 → self-wakes

Three atomic operations total. No spin loops, no mutex. Memory ordering: store(release) on resolve, load(acquire) on poll, fetch_or(acq_rel) on wake.

PromiseWaker — Same-Thread vs Cross-Thread

PromiseWaker wraps an Arc<_::WakeState> — the inner state {loop, woken, fiber} allocated on the heap and shared via atomic refcount. All copies share the same state.

void wake() const {
    if (m_state->loop == xEventLoopCurrent()) {
        if (m_state->fiber) {
            xFiberSwitch(m_state->fiber);   // Fiber: direct switch, no flag
        } else {
            m_state->woken = true;          // Non-fiber: set flag
        }
    } else {
        xEventLoopPost(m_state->loop,       // Cross-thread — MPSC enqueue
            &on_wake, &(*m_state));
    }
}

sizeof(PromiseWaker) = sizeof(Arc<_::WakeState>) = 8B. Same-thread fiber: direct xFiberSwitch. Same-thread non-fiber: flag set. Cross-thread: xEventLoopPost (lock-free MPSC).

PromiseContext is the non-cloneable poll handle that owns a PromiseWaker. It provides park() (fiber: single xFiberYield(); non-fiber: xEventLoopRun(X_RUN_ONCE) loop on woken flag) and waker() (returns const PromiseWaker& for resolvers to clone and store).

Nested wait() Correctness

xEventLoopRun does not call xEventLoopLeave on return. Enter/Leave is scoped to WaitScope:

class WaitScope {
public:
    explicit WaitScope(const EventLoop &loop) : m_loop(loop.handle()) {
        if (m_loop) xEventLoopEnter(m_loop);
    }
    ~WaitScope() {
        if (m_loop) xEventLoopLeave();
    }
};

Call stack for nested wait():

wait()  (outer)
  poll()  →  None
  xEventLoopRun()  →  timer fires → resolve → then() callback
    .then(fn)  →  fn calls inner_promise.await()
      wait()  (inner)
        poll()  →  Some(result)  →  return
    fn returns result
  woken==true  →  poll  →  Some  →  return

Both xEventLoopRun calls see the same thread-local loop handle. Neither inner Run exit unbinds it.

Integration Status

ConsumerNode TypeReason
async<T>()ManualResolveNode<T>Deferred resolve (ArcWeak, safe after destruction)
Promise::resolve(v)ImmediatePromiseNodeImmediate completion
Promise<void>::after(ms)AdapterPromiseNode<void, TimerAdapter>Timer-based delay
Promise<T>::work(fn)AdapterPromiseNode<T, WorkAdapter<T, F>>Thread-pool work
Promise<T>::adapt<Adapter>(args)AdapterPromiseNode<T, Adapter>Custom adapter
.then(fn)TransformPromiseNode / ChainPromiseNodeTransform / auto-flatten
yield()YieldPromiseNodeChain entry point
all(...)AllTuplePromiseNode / AllVoidPromiseNodeConcurrent — wait for all
race(...)RacePromiseNodeConcurrent — first wins