Proposal: Task and Async Context Definitions
Status: Approved Approved: 2026-01-29 Author: Eric (with AI assistance) Created: 2026-01-29 Affects: Compiler, runtime, concurrency model
Summary
This proposal formally defines what a “task” is in Ori and specifies async context semantics. Currently, the spec uses these terms without precise definitions, making it impossible to reason about concurrency guarantees.
Problem Statement
The specification references “tasks” in multiple places without defining them:
- “Types can safely cross task boundaries” (Sendable trait)
- “All spawned tasks complete before nursery exits” (nursery pattern)
- “Tasks may execute in parallel” (parallel pattern)
What is a task? The spec doesn’t say.
Similarly, uses Suspend declares “may suspend” but:
- What exactly is suspension?
- Where can suspension occur?
- How do async and non-async functions interact?
Definitions
Task
A task is an independent unit of concurrent execution with:
- Its own call stack — function calls within a task are sequential
- Isolated mutable state — no task can directly access another task’s mutable bindings
- Cooperative scheduling — tasks yield control at suspension points
- Bounded lifetime — tasks are created within a scope and must complete before that scope exits
Tasks are NOT threads. Multiple tasks may execute on the same thread (green threads/coroutines), or the runtime may distribute tasks across OS threads.
Async Context
An async context is a runtime environment that can:
- Execute async functions (those with
uses Suspend) - Schedule suspension and resumption
- Manage multiple concurrent tasks
An async context is established by:
- The runtime — when
@maindeclaresuses Suspend, the runtime provides the initial async context - Concurrency patterns —
parallel,spawn, andnurserycreate nested async contexts for their spawned tasks
A function declaring uses Suspend requires an async context to execute — it does not establish one. The Suspend capability indicates the function may suspend, requiring a scheduler to manage resumption.
Suspension Point
A suspension point is a location where a task may yield control to the scheduler. Suspension points occur ONLY at:
- Async function calls — calling a function with
uses Suspend - Channel operations —
sendandreceiveon channels - Explicit yield — within
parallel,spawn, ornurserybody evaluation
Suspension NEVER occurs:
- In the middle of expression evaluation
- During non-async function execution
- At arbitrary points chosen by the runtime
This provides predictable interleaving — developers can reason about atomicity.
Semantics
@main and Async
Programs using concurrency patterns (parallel, spawn, nursery) must have @main declare uses Suspend:
// Correct: main declares Suspend
@main () -> void uses Suspend = {
parallel(tasks: [task_a(), task_b()])
}
// ERROR: main uses concurrency without Suspend
@main () -> void = {
parallel(tasks: [task_a(), task_b()]), // Error: requires Suspend capability
}
The runtime establishes the async context when @main uses Suspend is declared.
Task Creation
Tasks are created by:
| Pattern | Creates Tasks? | Description |
|---|---|---|
parallel(tasks: [...]) | Yes | One task per list element |
spawn(tasks: [...]) | Yes | Fire-and-forget tasks |
nursery(body: n -> ...) | Yes, via n.spawn() | Structured task spawning |
| Regular function call | No | Same task, same stack |
Task Isolation
Each task has:
- Private mutable bindings —
let x = ...in one task is invisible to others - Shared immutable data — values passed to tasks are immutable from the task’s perspective (ownership transferred)
- No shared mutable state — Ori’s memory model prevents this
@example () -> void uses Suspend = {
let x = 0
parallel(
tasks: [
() -> {x = 1}, // ERROR: cannot capture mutable binding across task boundary
() -> {x = 2}
]
)
}
Async Propagation
A function that calls async code must itself be async:
@caller () -> int uses Suspend =
callee() // OK: caller is async
@caller_sync () -> int =
callee() // ERROR: callee uses Suspend but caller does not
@callee () -> int uses Suspend = ...
The async capability propagates upward — if you call async code, you must declare it.
Blocking in Async Context
A non-async function called from an async context executes synchronously, blocking that task (but not other tasks):
@expensive_sync () -> int =
// Long computation, no suspension points
heavy_math()
@main () -> void uses Suspend = {
parallel(
tasks: [
() -> expensive_sync(), // This task blocks during computation
() -> other_work(), // This task can run concurrently
]
)
}
Task Memory Model
Capture and Ownership Transfer
Task closures follow Ori’s standard capture-by-value semantics. When a value is captured by a task closure, the original binding becomes inaccessible — ownership transfers to the task:
@capture_example () -> void uses Suspend = {
let data = create_data()
nursery(
body: n -> {
n.spawn(task: () -> process(data)), // data captured by value
// data cannot be used here — ownership transferred to spawned task
print(msg: data.field), // ERROR: data is no longer accessible
}
)
}
This is not a new “move” mechanism — it uses the existing capture-by-value behavior with an additional constraint: bindings captured across task boundaries become inaccessible in the spawning scope to prevent data races.
Sendable Requirement
When spawning a task, captured values must be Sendable:
@spawn_example () -> void uses Suspend = {
let data = create_data(), // data: Data, where Data: Sendable
nursery(
body: n -> n.spawn(task: () -> process(data)), // OK: data is Sendable
)
}
Reference Count Atomicity
When values cross task boundaries (via spawn or channel send), reference count operations are atomic. This is an implementation requirement, not a language-level concern — the compiler/runtime must ensure thread-safe reference counting for values accessed by multiple tasks.
Examples
Valid Async Patterns
// Task creates subtasks
@fan_out (items: [Item]) -> [Result] uses Suspend =
parallel(
tasks: items.map(item -> () -> process(item: item)),
)
// Suspend function calling suspend function
@outer () -> int uses Suspend = inner()
@inner () -> int uses Suspend = fetch_data()
// Non-async helper in async context
@async_with_sync () -> int uses Suspend = {
let raw = fetch_raw(), // async call — suspension point
let parsed = parse(raw), // sync call — no suspension
validate(parsed), // sync call — no suspension
}
Invalid Patterns
// ERROR: sync function cannot call async
@bad_sync () -> int = fetch_data()
// ERROR: capturing mutable binding across task boundary
@bad_capture () -> void uses Suspend = {
let counter = 0
parallel(tasks: [() -> {counter = counter + 1}])
}
// ERROR: main uses concurrency without Suspend
@bad_main () -> void = parallel(tasks: [task_a()])
Spec Changes Required
New Section: XX-concurrency-model.md
Create new spec section covering:
- Task definition
- Async context definition
- Suspension points
- Task creation patterns
- Task isolation guarantees
- Async propagation rules
Updates to 14-capabilities.md
Clarify that Suspend is a marker capability indicating suspension possibility, with reference to the concurrency model spec.
Updates to 10-patterns.md
Add references to task definitions for parallel, spawn, and nursery.
Design Rationale
Why Explicit Suspension Points?
Languages with implicit suspension (like Go) make it hard to reason about data races. By limiting suspension to explicit points (async calls, channel ops), developers can understand where interleaving occurs.
Why Propagate Async?
This is the same rationale as Rust’s async fn — making suspension visible in the type system catches errors at compile time rather than runtime.
Why Ownership Transfer for Task Capture?
Preventing shared mutable state between tasks eliminates data races. The ownership transfer ensures the spawning code cannot accidentally modify data the task is using.
Summary
| Concept | Definition |
|---|---|
| Task | Independent concurrent execution unit with own stack and isolated mutable state |
| Async context | Runtime environment capable of scheduling async execution |
| Suspension point | Explicit location where task may yield (async calls, channel ops) |
| Task creation | Via parallel, spawn, or nursery patterns only |
| Capture | Values captured by value; binding becomes inaccessible; must be Sendable |
| Async propagation | Callers of async functions must be async |
| @main requirement | Must declare uses Suspend to use concurrency patterns |