stackless-async-context.ts woke two concurrent workers from 2 ms and 1 ms
timers. The deadlines were taken when each worker scheduled its timer, so
on a slow build both could be due on the same tick in either order. Those
workers now wake from gates that chained timer callbacks open one per
turn in a fixed order. Each woken body still crosses a timer and
nested awaits with its own AsyncLocalStorage context. The sequential
chain workers keep one pending timer at a time.
stackless-async-ordering.ts scheduled setTimeout(0) and setImmediate from
the main module, where Node leaves their order to timing; the lines never
printed only because main finished first. Main now awaits a timer whose
callback schedules the immediate, a microtask and a tick, so the
macrotask order is pinned and printed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stackless coroutine body cannot use the await-call entries, which park
the caller's stack, so `await f(...)` of an async f that never suspends
spawned f's promise and awaited it: 4M such awaits took 142 ms against
main's 91 ms. The coroutine body now calls f's inline await entry
(sc_ai_*), which runs the body in place between scr_async_inline_enter
and exit and leaves a throw pending. The coroutine then takes the await's
turn itself. A throw becomes the rejected promise the await re-throws
from on the same turn, and an adopting `return f(...)` first takes the
then-job turn, exactly as the await-call entries do (80 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native async bodies whose only suspension points are their own awaits now
compile to LLVM switched coroutines, the lowering WASI async bodies and
native synchronous generators already use. A suspended call keeps a heap
frame of the values live across its awaits instead of a fiber with its own
stack. Bodies that reach other suspension points (async generator
consumption, dyn awaits, node:test subtests), module evaluation bodies,
dyn results, worker and library programs, and Windows keep their fibers.
The emitter selects bodies by emitting them as usual first: that text may
suspend only through await runtime calls and await-call entries, and may
call no function that parks. The coroutine form is then emitted and
checked again. Each await prepares its continuation in the runtime (a
pending promise's waiter list, or one READY-queue turn, continuing in place
exactly where a fiber's hop would) and extracts the settled result after
resuming. The eager prefix runs on the spawner's stack and moves to a fresh
stack near the stack guard, so deep async recursion keeps working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>