Tasks

A Sec task is a language-level concurrent execution unit. Also known as a green thread. Tasks are intended for work that may execute independently, suspend, migrate between workers and complete later, while still participating in Sec's ownership, cancellation, cleanup and static-analysis rules.

Tasks are represented by the compiler-known generic type Task[T]. The type parameter T is the complete declared return type of the task function. A task returning Result[Image, IOError] is therefore represented as Task[Result[Image, IOError]]; the Result is not flattened into task failure.

fn Calculate() int {
    return 42
}

let worker := try spawn Calculate()
Core rule: A task handle represents both one logical task identity and ownership of its lifecycle responsibility. Task[T] is move-only and must not silently leave scope unresolved.

Purpose

Tasks provide a portable source-level model for concurrent work without fixing one universal runtime implementation. The same Sec task semantics may be implemented by a cooperative executor, worker pool, fibers, native threads, an RTOS task facility, an event loop, statically allocated task slots or another target-specific mechanism.

Those implementation choices must not change the language-visible types or lifecycle rules. In particular:

  • task creation is fallible;
  • successful creation produces an owning Task[T] handle;
  • the task handle is move-only;
  • terminal execution is represented by TaskOutcome[T];
  • cancellation, panic and task-runtime failure remain distinct;
  • the task function's own return value remains exactly T.

Logical Execution Unit

A task is not defined as an operating-system thread, process, fiber, coroutine frame, worker-pool job or event-loop callback. Those are possible implementation mechanisms, not source-level task identity.

This distinction matters because a task may move between physical workers. Its identity, cancellation state, owned values and current-task context belong to the logical task and must follow it across any migration permitted by the scheduler.

Rule: Do not infer task identity or task-local state from the physical thread currently executing the task.

Task[T]

Task[T] is the compiler-known generic owning handle for a task whose body returns T.

fn Calculate() int {
    return 42
}

let worker := try spawn Calculate()
// worker: Task[int]

The physical representation of a task handle is target- and runtime-specific. It may internally contain an index, pointer, runtime handle or another representation. None of those details may alter the source-level ownership or completion model.

Complete Return Type

The task type preserves the complete return type of the callable. This is especially important when the callable itself returns Result.

fn LoadImage() Result[Image, IOError] {
    // ...
}

let worker := try spawn LoadImage()
// worker: Task[Result[Image, IOError]]

The following types are not equivalent:

Task[Result[Image, IOError]]
Result[Task[Image], IOError]

The first means that the task was created successfully and its function later returns a Result[Image, IOError]. The second would mean that creation itself returned an application IOError, which is not the task model.

Task Creation

Task creation is fallible. Both the explicit task form and the promoted default task form have the same raw type:

spawn task Work()
spawn Work()

When Work() returns T, each expression has type:

Result[Task[T], TaskSpawnError]

Normal Sec error handling is used to unwrap or handle creation failure:

let worker := try spawn Work()

A creation failure happens before a task exists. It must therefore never be reported as TaskOutcome[T] and must not produce a fake task handle.

If the selected target has no task capability at all, compilation or target validation rejects the program. Lack of target support is not represented as an ordinary runtime TaskSpawnError.

TaskSpawnError

TaskSpawnError describes failures that prevent a task from being created. Its canonical variants are:

enum TaskSpawnError error {
    OutOfMemory
    ResourceLimit
    ExecutorUnavailable
    InvalidConfiguration
    NativeFailure
}
  • OutOfMemory — required creation memory could not be obtained.
  • ResourceLimit — a task, worker, slot or comparable runtime limit was exhausted.
  • ExecutorUnavailable — the configured task executor cannot accept the task.
  • InvalidConfiguration — the requested runtime configuration cannot be instantiated.
  • NativeFailure — a lower-level creation failure has no more specific task-spawn variant.

Once task creation succeeds, later task-execution failure belongs to TaskError, not TaskSpawnError.

Handle Ownership

Task[T] is move-only for every T. Even when T itself is trivially copyable, copying the task handle would duplicate lifecycle responsibility and is therefore invalid.

Moving a task handle transfers responsibility for the same task. It does not create, duplicate, restart or reschedule the task.

let worker := try spawn Calculate()

// Moving the handle transfers lifecycle ownership.
// The original binding becomes unavailable.

The ordinary ownership rules determine where an explicit move marker is required for reusable move-only sources. Fresh values may transfer directly where the ownership rules permit fresh-value transfer.

Lifecycle Responsibility

An owning task handle carries a lifecycle obligation. A local owning Task[T] must not simply reach scope exit unresolved.

The lifecycle obligation may be resolved by:

  • await;
  • join;
  • detach;
  • moving the owning task handle to another owner.

Moving the handle transfers the obligation. Observing status does not resolve it. Requesting cancellation does not resolve it either; cancellation is only a request until the task actually reaches a terminal state or another lifecycle action resolves the handle.

Lifecycle checking is path-sensitive. Resolving a handle on one branch is not enough when another reachable branch leaves it unresolved.

Parent and Child Tasks

A task created by another task may participate in a logical parent-child relationship under the structured-concurrency rules. That relationship is not based on physical thread identity.

A child may execute on another worker or migrate later. Structured lifetime rules, child-completion requirements and propagation policies are defined by the structured concurrency model rather than by worker placement.

Current Task Context

Code executing inside a logical task has a current task context. The source-visible observation surface is:

type TaskContext

impl Task {
    static fn Current() TaskContext
}

impl TaskContext {
    CancelRequested bool
}

TaskContext is immutable and non-owning. It does not contain lifecycle authority and cannot consume or await the current task. Its cancellation observation follows the logical task across permitted worker migration.

Code that is not executing as a task does not gain a fictitious task context merely because it runs on a physical thread that also happens to execute tasks.

Lifecycle States

A runtime may internally distinguish states such as queued, ready, scheduled, suspended or running. Those internal scheduler states are not the portable terminal outcome model.

At the language level, an already-created task terminates in exactly one category:

  • completed normally;
  • cancelled;
  • panicked;
  • failed at the task-execution level.

After a task becomes terminal, it must never resume execution.

TaskOutcome[T]

TaskOutcome[T] is the canonical value describing how an already-created task terminated.

type TaskOutcome[T] union {
    Completed(T)
    Cancelled
    Panicked(PanicInfo)
    Failed(TaskError)
}

This is deliberately not Result[T, TaskError]. Cancellation and panic are not ordinary task-runtime errors, and a normal function-level Err(E) is still part of normal completion when T itself is a Result.

Rule: TaskOutcome[T] must never be silently flattened to T, even when the compiler can prove that one particular task completes normally.

Completed(T)

Completed(value) means that the task function reached normal Sec completion and produced its declared return value. The runtime does not reinterpret that value.

fn LoadImage() Result[Image, IOError] {
    // ...
}

let worker := try spawn LoadImage()
let outcome := await worker

// outcome: TaskOutcome[Result[Image, IOError]]

A normal application-level error returned by the task is therefore represented as:

Completed(Err(IOError.InvalidValue))

It is not represented as:

Failed(...)

When T is move-only, the active Completed payload owns that value until ordinary union and ownership rules transfer it elsewhere.

Cancelled

Cancelled means that the task terminated through Sec's cooperative cancellation mechanism. It carries no fabricated T value.

Requesting cancellation and observing Cancelled are different events. A cancellation request does not guarantee immediate termination, and a task that never observes the request may still return normally as Completed(T).

Panicked(PanicInfo)

Panicked(info) means that a panic escaped the task execution boundary and the selected target/runtime panic policy permits that panic to be represented as a recoverable task outcome.

Panic remains distinct from cancellation, from a returned application error and from task-runtime execution failure. Targets with a hard-abort panic strategy follow the panic and target-profile rules; task semantics do not weaken such a policy.

Failed(TaskError)

Failed(error) means that task creation succeeded, a real task existed, and the task later failed at the execution or runtime mechanism level.

TaskError is intentionally separate from both TaskSpawnError and any application error type returned inside T. Its complete future variant inventory is not frozen by the task rulebook; code and tooling must not invent variants merely to fill a gap.

Await

Awaiting an owning Task[T] uses:

let outcome := await worker

The static result type is always:

TaskOutcome[T]

Await consumes the owning task handle, resolves its lifecycle obligation and transfers any terminal payload ownership into the resulting TaskOutcome[T]. It may physically suspend a logical task, park a worker, block a native thread or use another target-specific mechanism, but the Sec result type does not change.

match outcome {
    Completed(Ok(image)) => Use(image)
    Completed(Err(error)) => HandleIO(error)
    Cancelled => HandleCancellation()
    Panicked(info) => HandlePanic(info)
    Failed(error) => HandleTaskFailure(error)
}

Chapter 57 covers general await syntax and awaitable expressions in more detail.

Join

Joining synchronizes with task completion while preserving the owning task handle for terminal inspection.

join worker

After a successful join, the task is terminal. The handle remains available for permitted terminal inspection, but the same lifecycle capability may not be joined repeatedly as though each join created a new completion event.

Await and join therefore serve different ownership purposes: await consumes the handle and returns TaskOutcome[T], while join preserves a terminal handle. Both establish the completion synchronization required for visibility of task-produced memory effects.

Identity, Status and Value

Task identity uses a task-specific identity type such as TaskID. It is distinct from thread and process identities.

Concurrency handles may intentionally share familiar member names such as id, name, status, value, Observe() and platform. Shared names do not make tasks, threads and processes semantically identical.

A task's status is a snapshot. Reading it does not consume the task, transfer the completion payload or by itself establish the synchronization guarantees of await or join. A value exists only for normal completion.

Cancellation Requests

The owning task handle may request cooperative cancellation:

impl Task[T] {
    fn RequestCancel() void
}

Requesting cancellation does not consume the handle. Repeated requests are idempotent unless a more specialized cancellation API explicitly defines other behavior.

The task may continue until it reaches a cancellation observation point. Detailed cancellation semantics, propagation, shielding and cancellation checkpoints are covered in chapter 59.

Task Observers

A task may expose a non-owning observation capability through Observe(). The observer is conceptually a TaskObserver[T] or compiler-equivalent non-owning type; it must not become a second lifecycle owner.

An observer may inspect permitted metadata and may participate in observation-oriented operations where their owning rules allow it. It must not:

  • await and consume the task result as owner;
  • join as the owning lifecycle capability;
  • detach the task;
  • take a move-only completion value;
  • perform other owner-only lifecycle operations without an explicit rule granting them.

Destroying an observer does not affect task execution.

Detach

Detach explicitly transfers a task out of the current structured lifecycle obligation. It consumes the owning task handle.

For a task whose result requires no explicit disposal:

detach worker

A value-returning task requires explicit intent when its eventual result and terminal outcome will no longer be observed:

detach worker discard

Detach is only possible after task creation succeeded. It does not discard a TaskSpawnError retroactively. A detached task must also be independent of borrows whose source lifetime may end before the detached task finishes.

Arguments and Ownership Transfer

Values crossing a task boundary obey ordinary ownership plus the stricter transferability rules for concurrent execution.

let resource := OpenResource()
let worker := try spawn Use(<-resource)

After committed transfer, the original move-only source is unavailable. Sec never silently clones a move-only resource merely because another task needs it.

Copyable values may cross by copy where ordinary parameter rules and transferability permit it. References crossing a task boundary require proof that lifetime, mutability, aliasing, address stability and migration constraints remain valid.

Task Captures and Closures

A spawned lambda or closure crosses a task execution boundary. Its captures therefore obey both closure-capture rules and task transferability rules.

Ownership capture of a move-only value makes the source unavailable after transfer commits. Borrowed captures require the same lifetime and alias proofs as explicit reference arguments. A runtime must never make an invalid borrow safe by simply copying the reference into a task object that lives longer.

Detached closures require captures that remain valid independently of the source scope from which the task was detached.

Scheduling and Migration

Successful creation makes the logical task eligible for execution according to the selected scheduler and target profile. A task is not permanently tied to the first worker that runs it unless the active runtime model explicitly imposes that restriction.

Migration must preserve task identity, cancellation state, owned values and all language-visible task state. Backend-private scheduler states must not leak into portable Sec semantics merely because one runtime happens to expose them.

Memory Visibility

Task creation, execution, await, join, cancellation and terminal publication obey the Sec concurrency memory model. Ownership transfer alone does not authorize a data race.

Completion synchronization through await or join must make task-produced memory effects visible according to the concurrency memory model. A runtime may not publish terminal completion before the memory effects guaranteed by that completion edge are visible to the observer.

Task Resource Analysis

Recursive or dynamically repeated task creation is allowed only when the selected target and profile can support the resulting resource behavior.

An embedded or statically provisioned target may require the compiler to prove a finite upper bound for task creation. A hosted target may instead permit runtime growth and report exhaustion through TaskSpawnError.ResourceLimit.

Compile-time rejection of an unsupported resource model and runtime task creation failure are intentionally different mechanisms.

Target and Runtime Independence

Sec does not require one mandatory general-purpose task runtime. The active CompilationPlan selects the task capability, runtime model, resource policy and lowering strategy appropriate for the target.

A target without dynamic allocation may use static task slots or another compatible strategy. If no implementation can satisfy the required semantics, the configuration is rejected instead of silently changing the language model.

Rule: Backend implementation may change representation and scheduling strategy, but never the source types Task[T], Result[Task[T], TaskSpawnError] or TaskOutcome[T].

Diagnostics

Task diagnostics should preserve the semantic layer that failed. Useful diagnostics distinguish:

  • task creation failure from failure of an already-created task;
  • unresolved lifecycle ownership at scope exit;
  • attempted copying of the move-only task handle;
  • use after task-handle move;
  • invalid detach without explicit discard intent;
  • invalid cross-task references and the exact lifetime or transferability reason;
  • target capability absence from a runtime TaskSpawnError;
  • Completed(Err(E)) from Failed(TaskError).

Tooling should never describe a task that normally returned Err(E) as a task-runtime failure.

Current Implementation Status

The revision-2 task model is normative, while compiler implementation is currently partial.

Implemented frontend work includes:

  • compiler-known Task[T];
  • move-only task-handle treatment;
  • lifecycle checking on currently supported control-flow paths;
  • detach syntax and explicit discard forms;
  • spawned callable and lambda task forms;
  • partial current-task-context analysis;
  • compiler-known identities for TaskOutcome[T], TaskSpawnError, TaskError and PanicInfo;
  • await Task[T] consuming the handle and typing as TaskOutcome[T];
  • preservation of nested TaskOutcome[Result[V, E]];
  • LSP hover using the same semantic await type as the compiler.

The largest known frontend mismatch is task creation typing: some existing spawn paths still use the older direct Task[T] model instead of the normative raw Result[Task[T], TaskSpawnError] type.

Major remaining areas include:

  • converging every spawn form on the revision-2 fallible creation type;
  • runtime production of all four terminal outcome categories;
  • complete join and terminal-value extraction semantics;
  • full non-owning observer restrictions;
  • fully path-sensitive lifecycle analysis;
  • complete transferability checks for arguments, captures, borrows, detach and migration;
  • explicit task operations in Semantic IR;
  • target-selected lowering without a mandatory universal runtime.

Best Practices

  • Handle task creation failure separately from task execution outcome.
  • Keep the complete task function return type inside Task[T].
  • Do not treat Completed(Err(E)) as a task-runtime failure.
  • Resolve every owning task handle through await, join, detach or ownership transfer.
  • Use observers when you need non-owning status observation rather than duplicating lifecycle ownership.
  • Make detached result discard explicit when a value-returning task is intentionally ignored.
  • Transfer move-only values explicitly rather than inventing hidden clones.
  • Avoid cross-task borrows unless lifetime and migration safety are clear.
  • Treat cancellation as a cooperative request, not immediate termination.
  • Use await or join when you require completion synchronization and memory visibility.
  • Do not rely on a particular thread, worker pool or event loop as portable task semantics.

Summary

  • Task[T] represents one logical task and its owning lifecycle capability.
  • A task is not inherently an OS thread, process, fiber or event-loop callback.
  • T is the complete declared return type of the task callable.
  • Task creation is fallible and has raw type Result[Task[T], TaskSpawnError].
  • TaskSpawnError applies only when no task was created.
  • Task[T] is always move-only.
  • An owning local task handle must not leave scope unresolved.
  • Task identity and current-task context follow the logical task across permitted migration.
  • An already-created task terminates as normal completion, cancellation, panic or execution failure.
  • TaskOutcome[T] is Completed(T), Cancelled, Panicked(PanicInfo) or Failed(TaskError).
  • A task returning Err(E) normally produces Completed(Err(E)).
  • await Task[T] always produces TaskOutcome[T] and consumes the owning handle.
  • Join preserves a terminal handle while establishing completion synchronization.
  • Cancellation requests are cooperative and do not by themselves resolve lifecycle ownership.
  • Observers are non-owning and must never become a second lifecycle owner.
  • Detach consumes ownership and requires explicit discard intent for ignored value-returning tasks.
  • Task arguments and captures remain subject to ownership and transferability analysis.
  • Completion synchronization must publish task-produced memory effects correctly.
  • Task runtime strategy is selected by the target without changing portable Sec semantics.
  • The revision-2 task model is normative; current compiler support remains partial.