Concurrency

Overview

Elle provides colorless concurrency from a small set of primitives: fibers for cooperative multitasking with signals, coroutines for generators and iterators, and OS threads via spawn/join for true parallelism.

Fibers

A fiber is an independent execution context — its own stack, call frames, signal mask, and heap. Fibers are cooperative and explicitly resumed. When a fiber emits a signal, it suspends and the parent decides what to do next.

# Create a fiber that yields values
(defn produce []
  (emit :yield 1)
  (emit :yield 2)
  (emit :yield 3))

(def f (fiber/new produce |:yield|))

(fiber/resume f) (println (fiber/value f))  # => 1
(fiber/resume f) (println (fiber/value f))  # => 2
(fiber/resume f) (println (fiber/value f))  # => 3

When a fiber finishes, its entire heap is freed in O(1) — no GC pause, no reference counting.

Signals

Signals are typed, cooperative flow-control interrupts. A signal is a keyword — :error, :log, :abort, or any user-defined name — that a fiber emits to its parent. The parent's signal mask determines which signals surface.

# Error handling via signals
(defn risky [x]
  (if (< x 0)
    (error {:error :bad-input :message "negative input"})
    (* x x)))

(def f (fiber/new (fn [] (risky -1)) |:error|))
(fiber/resume f)

(if (= (fiber/status f) :paused)
  (println "caught:" (fiber/value f))
  (println "result:" (fiber/value f)))

The compiler infers which functions can emit signals and enforces that silent contexts don't call yielding ones.

Coroutines

Coroutines are lightweight cooperative sequences. coro/new wraps a function, yield suspends with a value, coro/resume steps forward.

# Fibonacci generator
(def fib-gen (coro/new (fn []
  (var a 0)
  (var b 1)
  (forever
    (yield a)
    (def tmp b)
    (assign b (+ a b))
    (assign a tmp)))))

# Pull first 8 Fibonacci numbers
(each _ in (range 8)
  (print (coro/resume fib-gen) " "))
(println)
# => 0 1 1 2 3 5 8 13

Threads

Use spawn to execute a closure in a new OS thread and join to wait for completion. Closures can capture immutable values; mutable values cannot cross thread boundaries.

# Parallel computation
(def h1 (spawn (fn [] (* 2 3))))
(def h2 (spawn (fn [] (* 4 5))))
(def h3 (spawn (fn [] (* 6 7))))

(println (+ (join h1) (join h2) (join h3)))  # => 68

Processes

Elle supports Erlang-style actors built on fibers. Processes communicate via send/recv, can be linked for crash propagation, and use trap-exit for supervision.

See examples/processes.lisp for a complete fiber-based process scheduler with message passing, links, and crash propagation.

Concurrency Primitives

FunctionDescription
(fiber/new fn mask)Create a fiber with a signal mask
(fiber/resume f)Resume a suspended fiber
(fiber/status f)Get fiber status (:new, :paused, :dead)
(fiber/value f)Get the last yielded/returned value
(emit signal value)Emit a signal to the parent fiber
(coro/new fn)Create a coroutine from a zero-arg function
(coro/resume co)Step a coroutine forward
(yield value)Suspend coroutine and yield a value
(spawn fn)Execute closure in a new OS thread
(join handle)Wait for thread to complete, return result
(signal name)Declare a user-defined signal keyword
(silence fn signals)Restrict which signals a function may emit