By Christian Findlay

Fibers and Concurrency

The native target provides Fiber<T> computations and buffered Channel<T> communication. A normal native fiber is backed one-to-one by a pthread; Osprey does not expose a separate OS-thread API. Managed values captured by spawn or sent through a channel may be co-owned across threads as specified by fiber boundary ownership. The WebAssembly target excludes this pthread runtime.

Default and ML syntax lower these forms to the same Expr::Spawn, Expr::Await, Expr::Yield, Expr::Send, and Expr::Recv nodes. See ML Flavor Syntax for ML parsing.

Sleep [CONCURRENCY-SLEEP]

sleep(milliseconds: int) -> Unit blocks the current native thread for the given number of milliseconds. A duration less than or equal to zero returns immediately. The runtime's integer status is not exposed.

Spawn and await [CONCURRENCY-SPAWN-AWAIT]

spawn expression captures the expression's free values, schedules it, and returns Fiber<T>, where T is the expression type. await fiber blocks until that computation finishes and returns its T value. Fiber<T> has no public record constructor; spawn is the construction operation.

fn work(value: int) -> Result<int, MathError> = value + 1

let task = spawn work(41)
let answer = await(task) ?: 0
work : int -> Result<int, MathError>
work value = value + 1

task = spawn (work 41)
answer = (await task) ?: 0

Under ARITH-CHECKED, task is a Fiber<Result<int, MathError>>: await returns that complete source type and ?: 0 explicitly selects the example's overflow policy. It never erases a Result channel. Each spawn site allocates a distinct capture cell. Pointer and floating-point values likewise return with their source type.

A Fiber<T> handle is reusable: awaiting the same completed fiber more than once MUST return the same T value on every call. For a managed T, every await produces an independently owned reference. One caller releasing its value MUST NOT invalidate a later await. Once every spawned computation is quiescent, normal program teardown MUST release the runtime's completed-result roots after language-owned values have dropped; it MUST NOT release a cached result while another fiber can still await it.

Buffered channels [CONCURRENCY-CHANNEL]

Channel(capacity) creates a FIFO channel whose positive integer capacity is the number of buffered values. Zero and negative capacities are rejected; the runtime does not implement rendezvous channels.

Operation Type Behavior
Channel(capacity) int -> Channel<T> Create a positive-capacity buffer; T is inferred from use.
send(channel, value) (Channel<T>, T) -> Unit Block while the buffer is full, then append value.
recv(channel) Channel<T> -> T Block while the buffer is empty, then remove its oldest value.

send is not a Result: its native status is internal to the runtime call and the language expression evaluates to Unit. recv directly returns T. A backend MUST preserve a Result<U, E> element as a complete value; a backend without a shape-aware channel ABI MUST reject Channel<Result<U, E>> at compilation. It must never unwrap or reinterpret the channel element.

let channel = Channel(3)

let producer = spawn {
    send(channel, 1)
    send(channel, 2)
    send(channel, 3)
}

let consumer = spawn {
    print("got ${recv(channel)}")
    print("got ${recv(channel)}")
    print("got ${recv(channel)}")
}

await(producer)
await(consumer)
channel = Channel 3

producer = spawn
    send channel 1
    send channel 2
    send channel 3

consumer = spawn
    print "got ${recv channel}"
    print "got ${recv channel}"
    print "got ${recv channel}"

await producer
await consumer

Yield and completion [CONCURRENCY-YIELD]

yield value evaluates value, offers the current pthread's remaining time slice to the scheduler, and returns the same value with the same type. Bare yield returns Unit. In deterministic mode the scheduler hand-off is skipped, but the value is still returned. The legacy fiber_yield(int) -> int built-in uses the same runtime operation.

fiberDone(fiber: Fiber<T>) -> int is a non-blocking completion probe. It returns 1 after completion, 0 while a normal threaded fiber is running, and the native runtime returns -1 for an invalid handle.

Deterministic execution [CONCURRENCY-DETERMINISTIC]

Native code may declare the runtime control function:

extern fn fiber_set_deterministic_mode(enabled: bool) -> int

When enabled before spawning, spawn queues computations without creating pthreads. await executes queued computations in spawn order through the requested fiber. This mode is sequential, not an interleaving scheduler; yield therefore cannot switch fibers. fiberDone returns 1 for a queued fiber because its following await is what drives execution.

Reserved select syntax [CONCURRENCY-SELECT-REJECT]

Both parsers reserve and lower select, but channel selection has no runtime semantics. The type checker rejects every select expression with `select` is not supported before code generation. Use explicit send, recv, and await operations.