Async/Await Guide

Ergonomic asynchronous programming in Turmeric using fibers and delimited continuations.

Overview

Turmeric's async/await syntax enables direct-style asynchronous programming for I/O-bound and concurrent tasks. The implementation builds on Phase 18's delimited continuations and integrates with Turmeric's effect system.

Quick Start

;; Define an async function
(async
  (def data (await (read-file "data.txt")))
  (def result (await (process-data data)))
  (println result))

;; Type: returns a Future<T>
(def fut (async
  (+ 1 (await (fetch 2)))))

;; Block until completion
(println (await fut))  ; => 3

Core Concepts

Fibers

A fiber is a user-space thread (lightweight thread) that: - Has its own execution state (implemented via delimited continuations) - Can yield (suspend) and resume - Runs on an OS thread managed by a scheduler - Has no separate OS stack (avoids thread creation overhead)

Async Blocks

Futures

Scheduling

The scheduler is single-threaded: all fibers run on one OS thread, avoiding data races. A multi-threaded scheduler (fibers on a thread pool) is a future direction.

Design Decisions

Decision Rationale
Fiber-based model Leverages delimited continuations; avoids C stack issues
await desugars to shift Integrates with existing CPS infrastructure
async desugars to reset Minimal new machinery; natural fit for the foundation
Future<T> as return type Composable, can be awaited or passed to other code
No implicit thread spawning Explicit control for predictable performance
Integration with defer Cleanup on fiber completion; consistent with scope model

Under the Hood

Desugaring

;; Surface syntax
(async (+ 1 (await (fetch 2))))

;; Desugars to (conceptually)
(reset
  (fn []
    (+ 1 (shift k
      (fiber-suspend (fetch 2) k)))))

;; The scheduler later resumes k with the result of (fetch 2)

Comparison to Threads

Feature Threads Async/Await
Model OS-level 1:1 threads User-space fibers
Overhead ~10-100μs per thread ~1μs per fiber
Scalability 100s-1000s max 100k+ feasible
Stack Real OS stack CPS-based (heap)
Use case CPU-bound parallelism I/O-bound concurrency

Common Patterns

Sequential Operations

(async
  (def a (await (fetch-a)))
  (def b (await (fetch-b)))
  (process a b))

Concurrent Operations

;; Fork two concurrent operations; wait for both
(async
  (let [fut-a (async (fetch-a))
        fut-b (async (fetch-b))
        a     (await fut-a)
        b     (await fut-b)]
    (process a b)))

Error Handling

Effects-based try/catch works within async blocks (see Effects System Guide):

(async
  (try-with
    (fn []
      (await (fetch-file "missing.txt")))
    (fn [e k]
      (match e
        (FileNotFound _) -> (continue k "default")))))

Send Requirements for Async Bodies

The Send-across-await rule

Every value whose binding is in scope at an (await ...) point within an inline (async (fn [] ...)) closure must be Send. Non-Send types include:

Type Reason not Send
rc<T> Single-threaded refcount; races on cross-thread resume
ref<T> Owning reference tied to the creating fiber
&T / &mut T Borrows; lifetime is fiber-bound
cont<T> Continuation captures C stack frame

The compiler enforces this conservatively: any binding in scope at the await is treated as live, whether or not it is actually used after the await.

;; ERROR TUR-E0022: rc<int> is not Send
(async (fn []
  (let [x (rc/of 42)]
    (await (async zero))  ;; x is in scope -- not Send
    (rc/deref x))))

;; OK: int is Send
(async (fn []
  (let [x 42]
    (await (async zero))  ;; x is in scope -- Send
    x)))

Workaround

Consume or drop non-Send values before the await:

(async (fn []
  (let [x (rc/of 42)
        v (rc/deref x)]   ;; extract the value first
    (rc/drop x)           ;; drop x before the await point
    (await (async zero))  ;; only v (an int) is in scope
    v)))

Scope

This check applies only to inline closures passed directly to (async (fn [] ...)). Pre-defined functions referenced as (async my-fn) are not re-elaborated and are not checked here; their bodies were compiled without async context. A future CPS-based implementation will close this gap.

I/O Bindings

Turmeric doesn't provide built-in async I/O primitives. Import via FFI:

;; Example: libuv or custom C bindings
(defn read-file [path]
  (with-future-from-ffi "libuv_read" path))

(async
  (println (await (read-file "data.txt"))))

See Also