Cancellation and Context
Cancellation in Sec is cooperative. One execution entity may request that another stop, but safe Sec code is not torn down at an arbitrary instruction. Cancellation becomes terminal only when the target execution observes the request and commits cancellation at an explicit check or a cancellation-aware operation boundary.
Sec deliberately separates two related but different mechanisms. Execution
cancellation applies to the current Task[T] or Thread[T].
Context cancellation is an operation-local capability used to propagate a
cancellable branch and optional deadline through APIs without granting a callee authority
to cancel its caller's execution identity.
RequestCancel() requests cooperative
cancellation. It does not consume a lifecycle handle and does not mean the target has
already stopped. Terminal cancellation is a separate committed control-flow event.
Purpose
Concurrent code needs a way to stop work that is no longer required without losing ownership, skipping cleanup or introducing unsafe asynchronous teardown. Sec's cancellation model therefore preserves deterministic destruction and explicit lifecycle ownership while still allowing waits and long-running operations to react to cancellation requests.
The model covers:
- requesting cancellation of tasks and threads;
- observing cancellation on the current logical task or physical thread;
- the terminating
cancelstatement; - cancellation-aware operation commit races;
- compiler-inferred requirements for cancellable execution;
Context,ContextSourceand deadlines;- deterministic cleanup and memory visibility.
Cooperative Cancellation
A cancellation request is state, not an asynchronous exception. Safe Sec does not
destroy a task or thread merely because another execution called
RequestCancel().
The target may react through:
- an explicit
CancelRequestedcheck followed bycancel; - a cancellation-aware wait such as
await, lock acquisition or a channel wait; - another operation whose rulebook defines a cancellation commit boundary.
Cancellation is distinct from a normal return, Err(E), panic, hard thread
termination, process termination and hardware reset. A timeout on one operation is
also not automatically terminal cancellation of the task or thread that performed it.
Task and Thread Identity
Sec supports cooperative cancellation for two execution identities:
Task[T]
Thread[T]
A task is a logical Sec execution identity. A thread is a physical/native execution identity. They remain distinct even when a task currently runs on that thread.
Task migration does not change task cancellation identity. Likewise, cancelling a task must not implicitly cancel its executor thread, and cancelling a physical thread must not silently rewrite the cancellation state of a logical task running on it.
RequestCancel()
Both owning handle types expose the same cancellation request operation:
impl Task[T] {
fn RequestCancel() void
}
impl Thread[T] {
fn RequestCancel() void
}
RequestCancel() is:
- cooperative;
- idempotent;
- non-consuming;
- a no-op after terminal completion.
let worker := try spawn Work()
worker.RequestCancel()
let outcome := await worker
Requesting cancellation does not resolve the lifecycle obligation. An owning task or thread handle must still be awaited, joined, detached or transferred according to the owning lifecycle rules.
Current Execution Contexts
Current-execution observation deliberately mirrors the two execution identities:
Task.Current().CancelRequested
Thread.Current().CancelRequested
The compiler-known views are:
type TaskContext
type ThreadContext
TaskContext is a non-owning view of the current logical task;
ThreadContext is a non-owning view of the current physical thread. Neither
view grants lifecycle ownership or cancellation authority over an owning handle.
Task.Current() is valid only where a logical task context exists.
Thread.Current() additionally depends on the selected target/profile
having a physical-thread context. The compiler must not fabricate one from the other.
The cancel Statement
The complete Sec 0.1 source syntax is:
cancel
cancel is a terminating statement, not an expression. It commits terminal
cooperative cancellation of the current cancellable execution entity.
If a logical task exists, bare cancel terminates that task. Otherwise, in
an explicit cancellable physical thread callable, it terminates that thread. Task
identity therefore takes precedence when task code is running on a worker thread.
if Task.Current().CancelRequested {
cancel
}
Merely reading CancelRequested does not terminate execution. Code may
observe the request, perform bounded cleanup or bookkeeping, and execute
cancel when it is ready to commit terminal cancellation.
Cancellation Cleanup
Terminal cooperative cancellation follows ordinary deterministic cleanup rules.
Active defer blocks run, owned values are destroyed and resources release
through their normal ownership paths.
A live MutexGuard[T] owned by the cancelling execution must therefore be
destroyed and release its lock before terminal cancellation completes.
cancel is invalid inside defer. Cleanup
code may not recursively replace the terminal control flow already performing that
cleanup.
Cancellation does not fabricate a normal result value and does not become an
Err(E) merely because the callable's declared return type is
Result[T, E].
Inferred Cancellation Requirement
Sec 0.1 has a compiler-inferred semantic effect meaning that a callable requires a
current cancellable execution context. There is no required source annotation such as
@cancellable.
fn Inner() void {
cancel
}
fn Outer() void {
Inner()
}
Both functions require current cancellable execution. The requirement propagates transitively through the semantic call graph, recursion, generic instantiation and indirect callable values. Storing a cancellation-dependent function in a function value must not erase the requirement.
Task.Current() carries the more specific requirement that a current
logical task exists. Thread.Current() instead carries a physical-thread
capability requirement.
Cancellation Points
A cancellation point is an operation boundary where pending current-execution cancellation may compete with the operation's own normal commit.
Examples include:
await;- cancellation-aware join;
- blocking channel send/receive;
- waiting
select; - mutex acquisition;
- timer and I/O waits;
- runtime/task yield where the owning rule defines cancellation awareness;
- target-declared interruptible waits.
Each owning rulebook defines exactly what counts as a successful operation commit. Cancellation does not invent a generic fallback result for every operation.
Commit Races
Every cancellation-aware operation must resolve to exactly one committed source-level outcome.
- If cancellation commits first, no partial operation result or ownership transfer may remain.
- If the operation commits first, its result and ownership effects are real.
- A later cancellation observation must not roll back a committed operation.
- Runtime wakeup races must not produce both outcomes.
- Runtime wakeup races must not lose both outcomes and abandon ownership/state.
This first-commit discipline is the common rule used by await, select, channels, mutex acquisition and other waits.
Await and Join
Cancellation of the waiting execution is different from cancellation of the execution being awaited or joined.
For consuming await, the await rulebook defines the stronger cleanup rule:
if caller cancellation wins before await commit, await retains lifecycle ownership,
requests cancellation of the child, waits for a terminal child outcome, destroys that
outcome exactly once and only then allows caller cancellation to complete.
Join remains distinct because it preserves the owning handle. Cancellation must not lose that handle in a completion/cancellation race.
Channels and Select
Cancellation must never partially transfer a channel message. If cancellation wins before channel commit, ownership remains according to the channel's pre-commit state. If the channel operation commits first, its exact send/receive result and ownership changes remain committed.
For select, a cancellation winner before branch commit means that no
branch may consume ownership, no message may transfer, no task/thread result may be
consumed and losing registrations must be removed.
Sec 0.1 channel APIs do not implicitly read a lexical Context. Channel
wait timeouts are composed explicitly through select and after.
Mutex Acquisition
Ordinary Mutex[T].Lock() acquisition and current-execution cancellation
compete at the acquisition boundary.
- If cancellation commits first, no
MutexGuard[T]exists. - If acquisition commits first, the caller owns the guard.
- If cancellation follows, deterministic cleanup releases the acquired guard.
A Context-aware lock additionally has a separate operation-local Context outcome. That does not merge Context cancellation with current task/thread cancellation.
Memory Semantics
Cancellation request state must be safely observable and data-race-free. The compiler and runtime may use atomics or another target primitive internally, but the source semantics remain the same.
A cancellation request does not automatically publish unrelated application data. Shared mutable data still requires a mutex, atomic, channel, completion synchronization or another canonical happens-before relation.
Context Model
Context is a separate capability model for operation-local cancellation
and deadlines. Sec separates observation/propagation from cancellation authority.
Contextis the observation and propagation capability.ContextSourceowns cancellation authority for one derived branch.- A callee receiving only
ref Contextcannot cancel its caller's branch. - Context termination does not itself terminally cancel the current task or thread.
Context Types
The canonical source-visible types are:
enum ContextError error {
// Explicit or inherited Context cancellation committed first.
Cancelled
// The effective Context deadline expired first.
DeadlineExceeded
}
@noCopy
type Context
@noCopy
type ContextSource
Context may be owned as a standalone value, moved to another owner and
borrowed as ref Context. It does not grant cancellation authority.
ContextSource owns exactly one child Context, cancellation authority for
that branch and the associated deadline/timer/cancellation registrations. A child
Context owned by a source cannot be moved out of it.
Context API
The canonical public surface is:
impl Context {
static fn Background() Context
fn WithCancel() ContextSource
fn WithTimeout(timeout: duration) ContextSource
fn WithDeadline(deadline: Instant) ContextSource
IsCancelled bool
Error Option[ContextError]
Deadline Option[Instant]
}
impl ContextSource {
Context ref Context
fn Cancel() void
}
Context intentionally has no Cancel() method. Cancellation
authority remains with ContextSource. Its Context property is
borrowed access to the child and has static type ref Context.
ContextSource.Cancel() is idempotent. It changes the still-existing child
Context to its terminal cancellation state when cancellation wins, but it does not
destroy either the Context or the source.
Context Hierarchy
Parent cancellation propagates downward to still-existing descendants. Cancellation does not propagate upward, and cancelling one child branch does not automatically cancel sibling branches.
let root := Context.Background()
let request := root.WithCancel()
let database := request.Context.WithTimeout(2)
A derived ContextSource depends on its parent Context lifetime and cannot
outlive that parent. This keeps the cancellation tree and borrowed Context identities
deterministic under ordinary ownership rules.
Deadlines and Timeouts
Context timeouts use Sec's existing lowercase duration type. Absolute
deadlines use Instant.
let child := parent.WithTimeout(5)
let deadlineChild := parent.WithDeadline(deadline)
A child cannot have an effective deadline later than an earlier parent deadline.
WithTimeout derives a monotonic deadline from the current monotonic time,
then applies the parent bound.
A Context has exactly one committed terminal cause: Cancelled or
DeadlineExceeded. Whichever commits first remains the immutable
Context.Error; a later cancellation or deadline expiry does not rewrite it.
Context vs Execution Cancellation
Passing ref Context to an operation does not make that Context the current
task or thread cancellation identity.
If a Context-aware operation sees its supplied Context cancel first, it returns or
propagates ContextError.Cancelled. If the effective deadline wins, it
returns or propagates ContextError.DeadlineExceeded. Neither event by
itself terminally cancels the current task or thread.
Current-execution cancellation remains independently active and, when it wins at a
cancellation point, follows terminal execution cancellation cleanup rather than being
converted into a ContextError.
Ownership and Lifetimes
Both Context and ContextSource are @noCopy.
Standalone Context values move according to ordinary Sec ownership rules. The child
Context inside a source is borrow-only outside that source.
fn QueryDatabase(ctx: ref Context) Result[Data, DatabaseError] {
let query := ctx.WithTimeout(2)
return ExecuteQuery(query.Context)
}
A function that only needs to observe or propagate Context should normally accept
ref Context. If it needs its own cancellation authority, it derives its
own child source instead of receiving authority over the parent's branch.
Destroying ContextSource destroys its child Context and associated
registrations according to ownership rules. Destruction itself is not a cancellation
event and must not synthesize ContextError.Cancelled.
Target and Runtime Requirements
The selected CompilationPlan determines how cooperative cancellation,
wakeups, timers and waiting operations are implemented. A target may use atomics,
scheduler state, RTOS primitives, native thread facilities or another mechanism as
long as the same Sec semantics are preserved.
Target/runtime choices must preserve data-race-free request observation, exactly-one commit at cancellation points, deterministic cleanup and exactly-one terminal Context cause. Unsupported target capabilities are diagnosed through target validation rather than by changing the source type model.
Diagnostics and Tooling
Useful diagnostics include:
cancel is not valid inside defer cleanup
call to ProcessBatch requires a current cancellable task or thread execution context
ContextSource.Context is borrowed from its ContextSource and cannot be moved out
Context does not grant cancellation authority; cancel the owning ContextSource instead
Tooling should preserve the symmetric Task/Thread API names while still showing their
distinct identities. LSP hover and completion should expose the canonical
RequestCancel(), CancelRequested, ContextError and
Context surfaces from compiler/Sema facts rather than maintaining a parallel model.
Implementation Status
The cancellation rulebook is normative revision 2.0. The current implementation is
partial and is tracked in governance/concurrency_cancellation.yaml.
Implemented today:
Task[T]andThread[T]expose compiler-knownRequestCancel().- The call has the exact zero-argument,
void, idempotent, non-consuming surface. - Sema and LSP preserve the canonical member identity and hover/completion information.
Still planned or incomplete:
Task.Current()and fullTaskContextcancellation observation;- full synchronization of
Thread.Current()/ThreadContext; - runtime cancellation state, wakeups and cancellation-point commit races;
- deterministic runtime cancellation cleanup and terminal outcome publication;
- compiler-inferred cancellation effects through the complete call graph;
- the complete
Context/ContextSource/ContextErrorsubsystem; - Semantic IR and target/runtime lowering for cancellation.
Best Practices
- Treat
RequestCancel()as a request, not proof that the target has stopped. - Resolve lifecycle ownership after requesting cancellation.
- Use
Task.Current()for logical task cancellation andThread.Current()for physical thread cancellation. - Use
cancelonly to commit terminal cancellation of the current execution. - Do not use
cancelinsidedefer. - Do not treat Context cancellation as terminal current-execution cancellation.
- Pass
ref Contextwhen a callee needs observation/propagation but not cancellation authority. - Derive a child
ContextSourcewhen a callee needs to cancel its own branch. - Use
durationandInstantfor timeout/deadline APIs; do not invent another duration type. - Remember that cancellation-state publication does not synchronize unrelated mutable application data.
- At cancellation points, reason in terms of operation commit versus cancellation commit, never partial success.
Summary
- Cancellation in Sec is cooperative and never arbitrary safe-code teardown.
Task[T]andThread[T]expose non-consuming, idempotentRequestCancel().- Task and thread cancellation identities remain distinct.
Task.Current().CancelRequestedobserves logical task cancellation.Thread.Current().CancelRequestedobserves physical thread cancellation.cancelterminally cancels the current task when one exists, otherwise an eligible explicit thread.- Terminal cancellation runs ordinary defer/destruction cleanup.
- Cancellation requirements are compiler-inferred and propagate transitively through callables.
- Cancellation-aware waits use an exactly-one commit discipline.
- Operation success cannot be rolled back after commit, and cancellation cannot leave partial ownership transfer.
- Context is an operation-local cancellation/deadline capability, not the current execution identity.
Contextgrants observation/propagation;ContextSourcegrants cancellation authority.ContextErroris exactlyCancelledorDeadlineExceeded.- Context terminal cause is first-wins and immutable.
- Parent Context cancellation propagates downward, never upward.
- Both Context and ContextSource are
@noCopy. - The child Context owned by ContextSource is borrowed through
ContextSource.Context. - Cancellation requests and Context state are safely observable but do not publish unrelated program data.
- The normative model is complete; current implementation support remains partial.