Spawn
spawn is Sec's explicit boundary between synchronous execution and newly
created concurrent execution. A normal call executes in the current execution entity.
Adding spawn requests a new task, thread or process and returns a handle that
represents the resulting lifecycle responsibility.
let value := Calculate() // synchronous call
let task := try spawn Calculate() // new task
spawn never means “run this somehow in the
background”. It requests one explicit execution kind, preserves the callable's complete
return type, has a fallible creation boundary and produces an owning lifecycle handle.
Purpose
Sec keeps asynchronous creation separate from function declaration. Functions do not become asynchronous because of their names, return types or internal behavior. The same callable may be invoked synchronously in one place and spawned in another.
The spawn boundary gives the compiler one precise point at which to validate ownership transfer, retained borrows, captures, lifecycle responsibility, target support and resource limits.
Syntax
spawn expression
spawn task expression
spawn thread expression
spawn process expression
Typical source code:
let worker := try spawn Work()
let taskWorker := try spawn task Work()
let threadWorker := try spawn thread Work()
let childProcess := try spawn process Program()
The initial model requires the operand to resolve to a callable expression. Named functions, methods and callable values may therefore be spawned when their ordinary call semantics are valid.
Default Task Spawn
Plain spawn creates a task and is exactly equivalent to
spawn task.
let first := try spawn Work()
let second := try spawn task Work()
If Work() returns T, both successful values have type
Task[T]. The raw expressions before try have type
Result[Task[T], TaskSpawnError].
Execution Kinds
The requested execution kind is part of source semantics. Task, thread and process creation are not interchangeable optimization choices.
spawn Work() // Result[Task[T], TaskSpawnError]
spawn task Work() // Result[Task[T], TaskSpawnError]
spawn thread Work() // Result[Thread[T], ThreadSpawnError]
spawn process Work() // process creation result defined by the process model
A backend must not silently lower spawn thread to an ordinary task or
spawn process to a thread. Unsupported execution kinds are rejected for
the selected target profile.
Contextual Modifiers
task, thread and process are contextual spawn
modifiers when they occur directly after spawn. They do not need to become
general-purpose reserved words merely because the spawn grammar uses them.
let thread := "worker"
let process := "encoder"
Callable Operand
The operand must resolve to a valid callable operation.
let first := try spawn Work()
let second := try spawn Process(value)
let third := try spawn service.Run()
let fourth := try spawn callback(value)
Ordinary overload resolution, receiver validation, argument typing, ownership rules
and effect validation still apply. spawn does not make an otherwise
invalid call legal.
Result Types
Creation is fallible. A successful spawn produces an execution handle; a failed spawn produces the creation error for that execution kind.
fn Calculate() int {
return 42
}
let calculation := try spawn Calculate()
The raw expression spawn Calculate() has type
Result[Task[int], TaskSpawnError]. After successful try,
calculation has type Task[int].
For physical threads, spawn thread Work() has raw type
Result[Thread[T], ThreadSpawnError].
Complete Callable Return Type
spawn preserves the callable's complete declared return type. It does not
unwrap Result, flatten nested result types or reinterpret application
errors as execution failures.
fn Load() Result[Image, IOError] {
// ...
}
let loading := try spawn Load()
The successfully created task has type:
Task[Result[Image, IOError]]
If Load() later returns Err(IOError.NotFound), that is a normal
return from the task function. Creation failure, task execution outcome and the
function's own Result remain distinct concepts.
Creation Failure
Creation failure occurs before an execution handle exists. It is therefore distinct from every later terminal state of successfully created work.
match spawn Work() {
Ok(worker) => {
Use(worker)
}
Err(error) => {
HandleSpawnError(error)
}
}
Resource exhaustion or another creation-time failure does not produce a failed
TaskOutcome[T]. No Task[T] exists in that branch.
Eager Creation
Task spawn is eager. After successful creation, the new task is scheduled, running or may already have completed. The implementation may begin executing the callable immediately.
Sec does not guarantee that the spawned callable executes before the next source statement. It does guarantee that successful creation has submitted the work to the selected task execution mechanism.
Thread-specific configuration may define additional start behavior such as deferred start. Those details belong to the thread chapter rather than to ordinary task spawn.
Synchronous Calls Stay Synchronous
let value := CalculateAsync() // synchronous
let task := try spawn CalculateAsync() // concurrent task
An Async suffix has no language meaning. Sec does not require
async fn merely so a function can be spawned. The concurrency boundary is
at the call site.
Argument Evaluation
Spawn arguments are evaluated in normal Sec evaluation order before ownership is transferred to the newly created execution entity.
let worker := try spawn Process(CreateFirst(), CreateSecond())
Successfully evaluated argument values are then copied, moved or retained as borrows according to their resolved types and parameter modes. The backend must not reorder observable argument evaluation across the spawn boundary.
Ownership Transfer
Owned parameters follow ordinary Sec copy and move rules. Spawning does not invent a hidden clone operation.
fn Process(data: Data) void {
// task owns data
}
let data := Data.Create()
let worker := try spawn Process(<-data)
If Data is move-only:
- ownership moves into the task;
- the caller may not use
dataafterward; - the task owns the value until it transfers or destroys it.
Copyable arguments may copy according to normal language rules. Move-only values are never silently duplicated because of spawn.
Borrowed Arguments
A spawned task may receive a shared borrow only when the compiler can prove that the referenced owner remains valid for the complete time in which the task may use it.
fn Inspect(data: ref Data) void {
// ...
}
fn Run() void {
let data := Data.Create()
let worker := try spawn Inspect(ref data)
let outcome := await worker
Handle(outcome)
}
The borrow remains active while the task may use it. The owner cannot be destroyed, moved or mutated in a conflicting way until task completion or another proven lifetime boundary.
Mutable Borrows
A ref mut argument gives the spawned task exclusive access for the
lifetime of the borrow.
fn Update(data: ref mut Data) void {
// ...
}
let mut data := Data.Create()
let worker := try spawn Update(ref mut data)
The parent may not access data directly or create another overlapping
borrow while the child may still use the mutable reference.
Escaping Spawned Work
A spawned task that borrows scope-owned data may not outlive that data. Returning, storing or otherwise transferring the task handle can therefore be invalid even when moving the handle itself would normally be allowed.
fn Invalid() Task[void] {
let data := Data.Create()
let worker := try spawn Inspect(ref data)
return worker
}
task worker cannot escape while borrowing local value data
Detached Work
Detached work cannot retain references whose owners may disappear while that work continues. The normal detached pattern is therefore to transfer ownership of the needed data into the execution entity.
fn Valid() void {
let data := Data.Create()
let worker := try spawn Process(<-data)
detach worker
}
Static or program-owned references still require valid concurrency-safe access. Static lifetime alone does not make shared mutation race-free.
Captures and Callable Values
A spawned lambda or callable value follows the normal capture rules. Captured values are copied or moved according to their types. Captured references must remain valid for the complete spawned lifetime.
MutexGuard[T] must not be captured, moved or
borrowed into another task. Mutex guards are task-bound.
Spawned Methods
let worker := try spawn service.Run()
Receiver handling follows the method signature:
- a value receiver is copied or moved;
- a shared receiver creates or propagates a shared borrow;
- a mutable receiver creates or propagates an exclusive mutable borrow.
The receiver must remain valid for the complete task use when its access is borrowed.
Current Execution Context
A newly spawned task executes with a new logical task context. Ordinary calls made by that task continue in the same task context until another explicit spawn boundary is crossed.
spawn thread creates a distinct physical thread context.
spawn process creates a separate process isolation domain. These are
different execution identities and handle types.
Parent and Child Tasks
When a task spawns another task, the currently executing task becomes the logical parent of the child.
fn Parent() void {
let child := try spawn Child()
let outcome := await child
Handle(outcome)
}
Parenthood does not replace ownership. The binding that receives the task handle remains responsible for resolving the child lifecycle.
Handle Ownership
Successful creation yields a move-only lifecycle owner: Task[T],
Thread[T] or the process-specific owning handle. The handle is not merely
an identifier; it represents unresolved lifecycle responsibility.
Moving the handle transfers that responsibility. A second owning copy must not be created merely because the underlying implementation might use a small machine handle.
Lifecycle Resolution
Spawned execution must not be silently forgotten. Before an owning handle leaves scope unresolved, it must be awaited, joined, detached, moved to another valid owner or consumed by another lifecycle operation defined for that execution kind.
spawn Work() // invalid when both creation result and lifecycle handle are ignored
This rule prevents implicit fire-and-forget execution.
Spawn in Expressions
spawn is an expression and may be used anywhere its fallible creation
result and resulting lifecycle ownership are valid.
let first := try spawn First()
let second: Task[int] := try spawn Second()
match spawn Third() {
Ok(worker) => Use(worker)
Err(error) => Handle(error)
}
Spawn in Loops
Spawning inside a loop is allowed, but every created execution entity must still have resolved ownership and bounded resource behavior.
The compiler should diagnose statically evident hazards such as:
- lost handles;
- detached spawn in unbounded loops;
- recursive spawn cycles;
- exponential child creation.
Target profiles may also impose task-count, thread-count or process-count limits.
Recursive Spawn
Recursive spawn is legal only when the selected profile permits it and creation is bounded.
fn Bomb() void {
let first := try spawn Bomb()
let second := try spawn Bomb()
await first
await second
}
This example creates an exponentially expanding task tree and should be diagnosed as unbounded task creation rather than accepted as ordinary recursion.
Scheduling Independence
Plain task spawn does not select a physical thread. A target may implement tasks with native threads, lightweight workers, cooperative scheduling, event loops, RTOS tasks or statically allocated state machines as long as source semantics remain unchanged.
By contrast, spawn thread explicitly requests a native physical thread
or the target profile's declared native-thread equivalent.
Creation Does Not Await Completion
A spawn expression may perform the work needed to create and schedule the new execution entity. It must not wait for the callable to finish.
If creation is unavailable or resources are exhausted, that condition is handled by the creation-failure model. Sec must not silently execute the whole callable synchronously as a fallback.
Cancellation Inheritance
A child task may inherit cancellation linkage or already-requested cancellation state from its parent according to the concurrency rules. Spawn must not silently clear cancellation state.
Detailed cancellation propagation is covered in chapter 59.
Target Support
The active target profile must support the requested execution kind. Unsupported kinds are compile-time target errors, not requests for silent fallback.
target profile does not provide task execution
target profile does not support physical threads
target profile does not support process creation
Static Analysis
The compiler must determine at least:
- the requested execution kind;
- the callable target and complete return type;
- the creation result and handle type;
- argument evaluation order;
- copied and moved values;
- retained shared and mutable borrows;
- captured values;
- the parent task where applicable;
- whether spawned work may escape;
- whether the selected target profile supports creation;
- whether lifecycle responsibility is eventually resolved.
These facts belong in semantic analysis and Semantic IR. The backend must not infer ownership or borrowing later from low-level loads and stores.
Diagnostics
spawned task result must be owned, awaited, joined or detached
task worker cannot escape while borrowing local value data
detached task worker cannot retain reference to local value data
cannot move value data into task because it is currently borrowed
cannot spawn task with MutexGuard[State]; mutex guards are task-bound
target profile does not provide task execution
unbounded recursive task creation detected: Bomb -> spawn Bomb
Diagnostics should identify both the spawn site and the ownership, borrow, lifecycle or target fact that makes the operation invalid.
Current Implementation Status
Spawn support is currently partial. The compiler recognizes spawned callable forms and already participates in task lifecycle analysis, but the complete fallible creation model has not yet converged across every execution kind.
Current task-spawn support includes:
- spawned named callables and callable values;
- spawned lambda forms;
- compiler-known
Task[T]; - move-only task lifecycle ownership;
- partial lifecycle and detach checking;
- ownership and capture information used by current semantic analysis.
The main known task-spawn mismatch is creation typing: some frontend paths still use
the older direct Task[T] model instead of the intended raw
Result[Task[T], TaskSpawnError] type.
Physical-thread creation is also incomplete. Thread[T] exists and is
move-only, but spawn thread has not yet fully converged on
Result[Thread[T], ThreadSpawnError], thread configuration, runtime
creation and lowering.
Process creation has a separate process rulebook. spawn process selects
that distinct process model rather than reusing task or thread semantics.
Best Practices
- Use a normal call for synchronous execution and
spawnonly for an intentional execution boundary. - Use plain
spawnfor logical tasks unless physical-thread semantics are actually required. - Handle creation failure separately from later task or thread outcome.
- Remember that
spawnpreserves the complete callable return type. - Move owned data into detached work instead of retaining short-lived borrows.
- Keep spawned borrows scoped tightly and synchronize before referenced owners become invalid.
- Never transfer a mutex guard across task boundaries.
- Resolve every owning execution handle explicitly.
- Review loops and recursion for bounded execution creation.
- Do not assume that task spawn creates a native thread.
- Do not rely on synchronous fallback when a target lacks the requested execution capability.
Summary
spawnis the explicit boundary between synchronous calls and newly created concurrent execution.- Plain
spawnis exactly equivalent tospawn task. spawn threadrequests physical-thread semantics.spawn processrequests the separate process model.- Task, thread and process handles are distinct types.
- Creation is fallible and occurs before an execution handle exists.
- Task creation has raw type
Result[Task[T], TaskSpawnError]. - Thread creation has raw type
Result[Thread[T], ThreadSpawnError]. Tis the complete callable return type and is never unwrapped by spawn.- Task spawn is eager by default.
- Normal calls remain synchronous regardless of function naming.
- Spawn arguments are evaluated normally before copy, move or borrow transfer.
- Move-only values are transferred, never silently cloned.
- Spawned borrows remain active until completion or another proven lifetime boundary.
- Detached or escaping work may not retain invalid short-lived references.
- Captures remain subject to ownership, borrowing and transferability analysis.
- Successful creation produces a move-only lifecycle owner that must be resolved explicitly.
- Loop and recursive spawn remain subject to bounded-resource analysis.
- The selected target profile must support the requested execution kind.
- Current compiler support remains partial, especially around fallible creation typing and thread lowering.