Skip to content

Coroutines for LDC

A breadth-first survey of coroutine implementation — both halves of the field — framed against one concrete engineering goal: giving LDC (the LLVM-based D compiler) suspendable, resumable functions, and in the same motion advancing the port of D to WebAssembly (and eventually WasmFX stack-switching). The survey splits along the field's one load-bearing axis. Stackless coroutines capture only the coroutine's own frame and become a compiler-built state machine; that is the stackless track. Stackful coroutines capture the whole call stack and become a fiber / green thread with a real, switchable machine stack; that is the stackful track. This page is the umbrella that ties the two tracks together; the deep-dives it points to are the source of truth.

Last reviewed: June 4, 2026


Thesis: one definition, two implementations

A coroutine is a function that can suspend — return control to its caller before it finishes — and later be resumed to continue from exactly where it left off. The only hard problem in building one is preserving in-progress state across the suspension: live locals, the position in the body, in-flight temporaries. There are precisely two strategies, and the entire survey is organised around them (concepts develops both in full):

  • Stackless — keep only the live locals of the coroutine's own frame in a compiler-synthesised struct (the coroutine frame), and rewrite the function into a state machine that re-enters itself at the right resumption point. The call stack is not captured; it has unwound back to the caller by the time the coroutine is suspended. This is what C++20 coroutines do and what LLVM's llvm.coro.* intrinsics lower — the substrate a D-on-LDC stackless port would target. Per-instance memory is exactly the across-suspend live set, "as small as a few bytes" (n4134:148). The cost: a coroutine can suspend only at a lexical await/yield point in its own body, never from inside an ordinary callee.
  • Stackful — keep the coroutine's whole call stack alive on the side. Suspension is a stack switch: swap the CPU stack pointer to a saved one and keep going. The state is a real machine stack, addressable and opaque to the compiler. This is what D's core.thread.Fiber does today (d-fiber). It can suspend "from nested stack frames" (n4134:106-108) — from arbitrary call depth, with no function colouring — at the cost of a whole reserved stack per instance (16 KiB + a guard page in D's Fiber; 1–2 MB by N4134's general-purpose default).

The names come from the only question that distinguishes them: does the saved state include the call stack, or not? N4134 fixes the definitions verbatim (n4134:102-108, quoted in concepts); every downstream design decision in this survey hangs on that single clause.

IMPORTANT

"Stackful coroutine" and "fiber" and "green thread" are the same concept at different layers. A fiber is the bare stackful coroutine (suspension primitive only). A green thread / virtual thread is a fiber plus a scheduler plus I/O integration — "a green thread is a stackful coroutine plus a scheduler plus I/O integration" (green-threads). D ships the bare fiber (core.thread.Fiber) and several green-thread runtimes built on it (vibe.d, Photon); it ships no compiler-lowered stackless coroutine at all. That gap is what this survey exists to map.


Catalog of approaches

One master table spanning both models, from the most portable to the most engine-dependent. Model is the stackless/stackful split; Lowering / runtime is where the suspension machinery is built; Frame / stack names the persistent-state model; wasm story is what reaches WebAssembly. Each approach is the subject of (or a thread through) the linked tracks.

ApproachModelLowering / runtimeFrame / stackPortable across DMD/GDC/LDC?wasm storyBest for
Portable frontend state machineStacklessshared DMD frontend (AST → state-object struct + switch)compiler-built frame struct; per-glue allocationYes — Rust/C#/Kotlin model; all three benefitplain wasm 1.0; no engine supporta language async/await / generator feature that must work everywhere
LLVM switched-resume (llvm.coro.id)StacklessLDC glue emits llvm.coro.*CoroSplit builds ramp/resume/destroyopaque coroutine-object handle; HALO-elidable to caller allocaLDC-onlyplain wasm via state machineC++20-shaped task/generator; turnkey path with mature elision
LLVM returned-continuation (coro.id.retcon[.once])StacklessLDC glue → CoroSplitcaller-owned fixed-size buffer; no implicit mallocLDC-onlyplain wasm via state machine@nogc / manual-allocation generators; weak post-inline elision
LLVM async (coro.id.async, swiftcc)StacklessLDC glue → CoroSplitcaller-allocated async context (heap linked-list); musttail transferLDC-onlyplain wasm via state machineexecutor-hopping async/await; closest analogue to WasmFX resumption
D Fiber (baseline; exists today)Stackfuldruntime runtime; hand-written asm fiber_switchContextfull machine stack per fiber (16 KiB + guard, fixed, never grows)Yes (ships now)noneassert(0, "Fibers not supported on WASI")suspend from arbitrary call depth; today's only built-in primitive
Go-style growable green threadsStackfulmanaged runtime: scheduler + copystack + precise stack mapscontiguous stack, starts ~2 KiB, doubles by copy + pointer-relocaten/a (a runtime model; infeasible for D's conservative GC)needs WasmFX or Asyncify (no native wasm stack)millions of cheap goroutine-style tasks — the model D cannot adopt
WasmFX engine continuations (future)Stackfulwasm bytecode + engineengine-managed one-shot stacks (cont); 1 type + 7 instructionsn/a (a wasm engine feature)requires Phase-3 engine + new LLVM/DMD backend supportstackful fibers, green threads, scheduler-heavy workloads on wasm

NOTE

Two rows describe models D cannot ship as-is. Go-style growable stacks need precise per-frame stack maps to relocate interior pointers on every copystack move (stack.go:733, adjustframe); D's GC is conservative and C-ABI callees are never mapped, so D's fiber is permanently stuck with fixed reserved stacks (stack-management §6). WasmFX is a future engine primitive, not in the LLVM wasm backend today. Both are in the catalog because the survey's wasm goal spans both models. See stack-management and wasmfx-target.


Taxonomy by model

The catalog collapses onto the one axis that decides everything: who captures what? Each row isolates a property a D coroutine design must reason about. The full treatment with verbatim N4134 / Coroutines.rst quotes is in concepts.

AxisStackless coroutineStackful coroutine / fiber
Who captures whatonly this coroutine's own frame (live across-suspend locals)the full call stack — "enabling suspension from nested frames" (n4134:106-108)
Suspend scopeonly at lexical await/yield points in this coroutine's bodyanywhere, including deep inside ordinary nested calls
Per-instance memorythe compiler-computed minimum live set — "as small as a few bytes" (n4134:148)a whole reserved stack: 16 KiB + guard in D's Fiber; 1–2 MB by N4134's default
Frame / stack sizingderived from def-use chains across suspends (Coroutines.rst:339-346)fixed at creation; over-provisioned; overflow traps on a guard page (no growth for D)
@nogcyes — frame is a plain struct, caller-placeable / HALO-elidable, no GCno — constructor mmaps a stack + GC-allocates a StackContext (d-fiber)
Who lowers itthe compiler (frontend state machine, or LLVM CoroSplit) — visiblethe runtime (asm fiber_switchContext) — opaque to the optimizer
wasma state machine in linear memory; the only viable path on wasm todayneeds WasmFX / Asyncify — D's Fiber assert(0)s on WASI (d-fiber, wasm)
Function colouringyes — "is a coroutine" / "must await" is a static, signature-level factno — any function may yield

When to use which

Reach for stackless when suspension points are statically visible (generators, async/await), when memory density matters (millions of in-flight tasks, one per I/O operation), when @nogc / -betterC / wasm are targets, or when the compiler should see through the suspension (thread migration, precise GC scanning). Reach for stackful when you must suspend from arbitrary call depth behind code you do not control — a synchronous library, a deep recursion, any ordinary callee that may block — which is the one axis where stackful wins outright and stackless cannot follow. The pragmatic D synthesis: stackless is the near-term portable win and the only route to D coroutines on wasm; the stackful Fiber stays the primitive for direct-style "suspend anywhere" concurrency and is what WasmFX is designed to make cheap on wasm. The two are complementary tracks, not competitors — the orthogonality argument is in wasm.


Milestones

A unified timeline interleaving the stackless lineage (the C++ paper evolution that produced the llvm.coro.* vocabulary, plus LLVM/Swift as the production consumers) and the stackful lineage (the fiber / green-thread runtimes), with D and wasm columns throughout. Per-track detail and the full citation tables live in the deep-dives (cpp for the C++ papers; context-switching, green-threads, stack-management for the fiber lineage).

DateStackless lineage (C++ / LLVM / Swift)Stackful lineage (fibers / green threads)Dwasm
~2003POSIX ucontext (getcontext/makecontext/swapcontext) — the portable fiber primitive
~2000sWindows Fibers (CreateFiber/SwitchToFiber); Boost.Context later supersedes both for speed
2008ucontext marked obsolescent in POSIX.1-2008 — pushing libraries to hand-written asm
2009Go goroutines ship (segmented stacks); the canonical M:N work-stealing runtime
2013N3722 "Resumable Functions"; N3858 weighs stackful "side stacks" vs stackless framesRust drops segmented stacks + green threads, standardises on 1:1 OS threadsD's core.thread.Fiber is the established stackful primitive
2014N4134 (Nishanov) mandates stackless ("Design goals ... necessitates stackless", n4134:138-140)Go 1.3 abandons segmented for contiguous, copying stacks (the "hot-split" fix)
2017Coroutines TS N4680co_await/co_yield/co_return; LLVM llvm.coro.* + CoroSplit ship
2019P0913 symmetric transfer; P0981 HALO; promise interface finalised (N4775)
2020C++20 coroutines standardised (P0057r8); Swift async lowering (coro.id.async, swiftcc)Fiber + std.concurrency.Generator are the only suspension story (both stackful)LLVM coro → plain wasm works (passes are pre-ISel)
2021OCaml 5 effect handlers = one-shot delimited continuations (heap fiber stacks); Eio builds on them
2023Java 21 virtual threads (Loom, JEP 444) GA — continuation + ForkJoinPool schedulerWasmFX stack-switching reaches Phase 3 (1 type + 7 instructions)
2026P1745's suspend_point_handle split remains post-C++20 / unshippedGo contiguous stacks, Loom, Eio mature; D's fiber stays fixed-stack (conservative GC)Still no coroutine syntax; LDC emits no llvm.coro.* (greenfield)LDC wasip1/p2/p3 + emscripten codegen tested; no WasmFX in the LLVM wasm backend

NOTE

The two lineages converge on the same primitive from opposite ends. OCaml 5's effect handlers (2021) expose the stackful continuation as a public, user-level primitive, and WasmFX is effect handlers lowered into the wasm engine (green-threads §4, wasmfx) — the direct conceptual ancestor of the wasm stack-switching proposal. Meanwhile the stackless lineage runs N4134's stackless mandate (2014) → the llvm.coro.* vocabulary → Swift's async lowering. A D-on-LDC design reuses both: the C++20 promise+awaiter+handle shape that LLVM was engineered to lower (stackless), and the Fiber → WasmFX retargeting path (stackful).


Document map

The survey is two tracks plus two cross-cutting leaves.

Cross-cutting

DocumentOne-lineLink
Concepts & vocabularyStackless vs stackful, the coroutine frame, suspend points, the suspend-scope limitation, ramp/resume/destroy, the thesisconcepts
wasm & WasmFXLLVM coro → plain wasm (today); Asyncify / JSPI stopgaps; WasmFX stack-switching (future, stackful); orthogonalitywasm

Stackless track

The compiler-built state machine: capture only the coroutine's own frame. Index: stackless-index.

DocumentOne-lineLink
LLVM coroutine modelThe three (four) ABIs, the llvm.coro.* intrinsic catalog, the switched-resume transform, HALO, custom ABIsllvm-coroutines
LLVM coro internalsCoroEarly/CoroSplit/CoroElide/CoroCleanup pass internals, frame building, the undocumented intrinsicsllvm-internals
C++20 coroutinesThe design template: promise + awaiter + handle, symmetric transfer, HALO, the construct→intrinsic bridgecpp
Cross-language comparisonHow Rust, C#, Kotlin, Swift, Python, JS, Go lower coroutines; frontend-state-machine vs LLVM-intrinsic; design lessons for Dcomparison
D language designCandidate D surfaces (generator / async / library), frontend lowering precedents, the frontend-vs-glue splitd-design
LDC code generationWhere coro.id/begin/suspend/end would be emitted; pragma(LDC_intrinsic) / inline-IR; the closure-frame analogldc-codegen
Attributes & memory@nogc/@safe/nothrow/pure/scope × a heap frame; checkClosure/setGC; custom allocator and HALO escape hatchesattributes
RoadmapThe synthesized, milestoned plan to add stackless coroutines to LDC and to wasmroadmap

Stackful track

The real switchable machine stack: capture the whole call stack. A fiber / green thread. Index: stackful-index.

DocumentOne-lineLink
D fiber baselinecore.thread.Fiber (stackful) + std.concurrency.Generator; sizing, GC scan coupling, the TLS-migration hazard, WASI assert(0)d-fiber
Context-switching mechanicsfiber_switchContext stack priming + register save/restore + SP/IP swap; ucontext, Windows Fibers, Boost.Context referencescontext-switching
Green threads / M:N runtimesGreen thread = stackful coroutine + scheduler + I/O integration; Go G-M-P, Loom, GHC, Eio; where D sitsgreen-threads
Stack managementFixed-reserved vs segmented vs contiguous-growable; the hot-split cliff; why Go's copystack is infeasible for D's conservative GCstack-management
WasmFX as a targetLowering a stackful fiber onto cont.new/resume/suspend; the engine-managed one-shot stack; what LLVM/DMD would needwasmfx-target

Cross-tree, the parallel algebraic-effects corpus and async-io survey are siblings: the WasmFX deep-dive is the authority on engine stack-switching, OCaml effects and Java Loom cover continuation-based suspension, effects & event loops and the Go netpoller tie coroutine resumption to completion-driven I/O, and the D landscape maps D's existing fiber-based async ecosystem.


Suggested reading paths

  • "Stackless / adding async-await to LDC." conceptsstackless-indexroadmap. The compiler-built state machine, from the model to the concrete plan.
  • "Stackful / fibers & green threads." conceptsstackful-indexd-fibergreen-threads. The switchable machine stack, from D's baseline up to full M:N runtimes.
  • "I want the wasm story." wasmwasmfx-targetwasmfx. Plain-wasm stackless coroutines today; WasmFX engine continuations for stackful fibers later.
  • "Just give me the plan."roadmap.

Sources

Each deep-dive carries its own citations; the authoritative artifacts behind this umbrella's synthesis are:

  • N4134, "Resumable Functions v.2" (Nishanov & Radigan, 2014) — the stackless definitions and the stackless-mandate argument: $REPOS/papers/n4134. The full C++ paper lineage (N3722, N3858, N4680, N4775, P0057r8, P0913, P0981, P1745) is tabled in cpp.
  • LLVM 23.0.0git ($REPOS/llvm-project): llvm/docs/Coroutines.rst, llvm/include/llvm/IR/Intrinsics.td, llvm/lib/Transforms/Coroutines/, llvm/include/llvm/Transforms/Coroutines/CoroShape.h, llvm/lib/Passes/PassBuilderPipelines.cpp, llvm/include/llvm/IR/IntrinsicsWebAssembly.td — surveyed in the stackless track.
  • LDC v1.42 ($REPOS/dlang/ldc): runtime/druntime/src/core/thread/fiber/{base.d,package.d,switch_context_asm.S,switch_context_riscv.S}, runtime/druntime/src/core/thread/context.d, runtime/phobos/std/concurrency.d, gen/, driver/main.cpp — the stackful baseline and the LDC wasm/codegen surface.
  • Go runtime ($REPOS/go/go/src/runtime): proc.go, stack.go, runtime2.go, asm_amd64.s, HACKING.md — the M:N scheduler and contiguous-growable stacks.
  • WasmFX / stack-switching ($REPOS/wasm/stack-switching/proposals/stack-switching/Explainer.md and examples/*.wast) and the survey's WasmFX deep-dive.
  • Cross-tree corpus: the algebraic-effects and async-io surveys.