Channels and Select
Sec channels provide typed, in-process ownership transfer between concurrent execution
entities. They are the main message-passing primitive for tasks and threads. The
companion select statement waits on multiple concurrent operations and
commits exactly one of them.
This chapter covers the ordinary Sec 0.1 channel model and the parts of
select that matter most when working with channels. It does not replace
the separate concurrency chapters on tasks, cancellation, mutexes, atomics or IPC.
Channel[T] is a typed, in-process,
move-only message channel with one receive capability and one or more send
capabilities. Blocking operations are cancellation points. Non-blocking operations are
not. select observes readiness non-destructively and commits exactly one
branch.
Purpose
Message passing is often simpler and safer than shared mutable state. A channel lets one execution entity transfer ownership of a value to another without sharing the same mutable object directly.
Sec channels deliberately model ownership transfer exactly. When a send commits, ownership of the message moves into the channel or directly to a rendezvous receiver. When a receive commits, ownership moves out to the receiver. Result types always show whether ownership moved or was returned to the caller.
In-Process Channels
Ordinary Channel[T], Sender[T],
Receiver[T] and MessageTicket[T] are strictly
in-process capabilities. They do not automatically become IPC.
task -> task
task -> thread
thread -> task
thread -> thread
If communication must cross a process boundary, Sec uses the separate IPC model from
chapter 66. Ordinary channels are not ProcessTransferable and are never
silently lowered to process messaging.
Core Types
The compiler-known source-visible channel surface in Sec 0.1 includes:
Channel[T]
Sender[T]
Receiver[T]
MessageTicket[T]
ChannelCapability
ChannelOptions
ChannelStatistics
SenderID
ChannelSendResult[T]
ChannelTrySendResult[T]
ChannelTryReceiveResult[T]
ChannelRevocableSendResult[T]
ChannelRevokeResult[T]
MessageDisposition
The public container type is Channel[T]. It is @noCopy
and exposes exactly two public fields: Tx and Rx.
@noCopy
type Channel[T] struct {
Tx: Sender[T]
Rx: Receiver[T]
try init(capacity: uint) AllocationError
try init(options: ChannelOptions) AllocationError
}
The hidden queueing and synchronization state is part of the implementation, but it is not a third public field and must not leak into the source model.
Construction and Options
Channel construction is fallible only through AllocationError when the
selected profile requires dynamic allocation or cannot prove construction infallible.
let messages := try Channel[string](capacity: 16)
let jobs := try Channel[Job](options: ChannelOptions {
Capacity: 64,
MaxSenders: 8,
Capabilities: { ChannelCapability.Statistics }
})
ChannelOptions contains exactly three public fields:
Capacity: uintMaxSenders: uintCapabilities: set[ChannelCapability]
MaxSenders must be at least 1. The option fields that fix
endpoint identity and optional runtime metadata must be known before lowering.
Optional Capabilities
Channels can enable a closed set of optional features through
ChannelCapability.
enum ChannelCapability {
Revocation
Expiration
ISRSafeSend
Statistics
}
RevocationenablesSendRevocable(...)and message tickets.Expirationenables message lifetime overloads.ISRSafeSendallows target-validated bounded non-blockingTrySendfrom ISR context.Statisticsenables statistics snapshots.
There is no general channel priority capability in Sec 0.1. Priority between
competing operations is expressed through source order in select, not by
a per-channel priority configuration.
Sender[T]
Sender[T] is move-only. It owns one send capability for one channel
identity.
@noCopy
type Sender[T]
impl Sender[T] {
fn Share() Option[Sender[T]]
fn Send(message: T) ChannelSendResult[T]
fn Send(message: T, lifetime: duration) ChannelSendResult[T]
fn TrySend(message: T) ChannelTrySendResult[T]
fn SendRevocable(message: T) ChannelRevocableSendResult[T]
fn SendRevocable(
message: T,
lifetime: duration
) ChannelRevocableSendResult[T]
fn Statistics() ChannelStatistics
fn Close() void
}
Share() does not consume the original sender. Close()
consumes the local sender capability. Blocking Send(...) is a waiting
operation; TrySend(...) is the non-blocking counterpart.
Receiver[T]
Receiver[T] is also move-only. Sec 0.1 defines exactly one live receive
capability for an ordinary channel.
@noCopy
type Receiver[T]
impl Receiver[T] {
fn Receive() Option[T]
fn TryReceive() ChannelTryReceiveResult[T]
fn Discard() void
fn Statistics() ChannelStatistics
fn Close() void
}
Discard() does not consume the receiver. Close() does.
A normal blocking receive returns Option[T]: Some(value)
means one message was received; None means the send side is closed and
the channel is drained.
Send Result Types
Sec uses exact result types so the caller can see whether ownership transferred or was returned.
Blocking send
type ChannelSendResult[T] union {
Sent
Closed(T)
}
Sent means the send committed. Closed(T) means the receive
side closed before commit, so ownership of the unsent message returns to the caller.
Non-blocking send
type ChannelTrySendResult[T] union {
Sent
WouldBlock(T)
Closed(T)
}
WouldBlock(T) is used when the channel is open but the send cannot commit
immediately. It returns ownership of the unsent value. This is especially important
for bounded channels and rendezvous channels with no immediately matching receiver.
Receive Result Types
Blocking Receive() returns Option[T]. Non-blocking receive
has its own exact result type.
type ChannelTryReceiveResult[T] union {
Received(T)
Empty
Closed
}
Received(T)means a deliverable message was received immediately.Emptymeans the channel is open but no deliverable message is available immediately.Closedmeans the send side is closed and no deliverable queued message remains.
Closed carries no payload because there is no message to transfer.
Revocable Messages
If a channel enables ChannelCapability.Revocation, the sender may use
SendRevocable(...). A successful revocable send returns a
MessageTicket[T] that owns the right to attempt later revocation.
type ChannelRevocableSendResult[T] union {
Accepted(MessageTicket[T])
Closed(T)
}
type ChannelRevokeResult[T] union {
Revoked(T)
Unavailable(MessageDisposition)
}
MessageTicket[T] is move-only and Revoke() consumes it.
Revocation is ticket-owned, not sender-owned.
match ticket.Revoke() {
Revoked(message) => {
Recover(<-message)
}
Unavailable(disposition) => {
HandleDisposition(disposition)
}
}
If revocation loses the race, the result reports a terminal
MessageDisposition: Received, Expired or
Discarded.
Expiration and Statistics
If ChannelCapability.Expiration is enabled, send and revocable-send
overloads may attach a message lifetime using duration. Expiration is a
message property, not a select timeout.
If ChannelCapability.Statistics is enabled, sender and receiver expose a
statistics snapshot. The public type is:
type ChannelStatistics struct {
Sent: uint64
Received: uint64
Revoked: uint64
Expired: uint64
Discarded: uint64
}
The counters are saturating snapshots. They are useful for instrumentation and observability, not as a synchronization substitute for actual program logic.
Close, Discard and Lifecycle
Closing sender capabilities eventually closes the send side. Closing the receiver closes the receive side immediately. Discarding drops queued deliverable messages from the receive side without consuming the receiver itself.
Sender[T].Close()consumes that sender capability.Receiver[T].Close()consumes the receiver capability.Receiver[T].Discard()preserves the receiver capability.
A blocking receive returns None only when the send side is closed and
the channel is drained. A non-blocking receive returns Closed under the
same fully drained closed condition.
FIFO Ordering and Rendezvous
Ordinary Sec channels are FIFO by successful message commit order. One sender preserves its own successful commit order, while concurrent senders are ordered by the actual global channel commit order.
A channel may be buffered or rendezvous-style depending on capacity and runtime realization. The source-level rules stay the same: send and receive commit at a definite point, and the exact result type reports success, closure or immediate inability to proceed.
Revoked, expired and discarded messages are skipped without reordering the remaining deliverable committed messages.
Cancellation and Waiting
Waiting channel operations are current-execution cancellation points. That includes
blocking Send(...), blocking Receive() and blocking
select. The non-waiting Try... operations are not
cancellation points.
Sec 0.1 does not define SendTimed, ReceiveTimed or
ref Context overloads on ordinary channels. Timeouts are composed through
select and after branches instead.
Select Basics
select waits on multiple concurrent operations and executes exactly one
branch.
select {
message := rx.Receive() => {
Process(message)
}
after 100ms => {
HandleTimeout()
}
}
A single execution of select chooses at most one branch. After the
chosen branch body completes, control continues after the statement.
Selectable Branches
For channels, the most common select branches are receives, sends, revocable sends, timeout branches and a final optional default branch.
Receive branch
select {
result := rx.Receive() => {
match result {
Some(message) => Process(<-message)
None => HandleClosed()
}
}
}
Send branch
select {
result := tx.Send(<-message) => {
match result {
Sent => HandleSent()
Closed(returned) => Recover(<-returned)
}
}
}
Revocable send branch
select {
result := tx.SendRevocable(<-message) => {
match result {
Accepted(ticket) => Store(<-ticket)
Closed(returned) => Recover(<-returned)
}
}
after 20ms => {
HandleTimeout(message)
}
}
When the send or receive branch is not selected, it must not consume ownership or mutate observable channel state.
Readiness and Commit
Every selectable operation has two conceptual phases:
readiness
commit
Readiness answers whether an operation could proceed. Commit performs the operation, transfers ownership and publishes the branch result. Only the selected branch commits. Non-selected branches remain unconsumed and must not change observable state.
Source-Order Priority
Branches are considered in source order. If multiple branches are ready at the same selection point, the first ready branch wins. This is an intentional source-visible priority rule, not an implementation accident.
select {
critical := criticalRx.Receive() => {
HandleCritical(critical)
}
normal := normalRx.Receive() => {
HandleNormal(normal)
}
}
If both receives are ready, the critical branch is selected. Sec 0.1
does not inject hidden fairness that overrides source order. A later branch may
starve if an earlier branch is continuously ready.
Default and Timeout
A timeout branch uses after duration => {{ ... }}. The timeout begins
when execution enters the select. It uses a monotonic time source.
select {
message := rx.Receive() => {
Process(message)
}
after 100ms => {
HandleTimeout()
}
}
A default branch is optional and executes only when no preceding
selectable branch is ready. If present, it must be the final branch and the select
does not wait.
select {
message := rx.Receive() => {
Process(message)
}
default => {
DoOtherWork()
}
}
A positive timeout branch together with a final default branch is normally a static error because the timeout can never be selected when no earlier branch is ready.
Ownership in Select
select must preserve ownership exactly. If a branch commits, it may
transfer ownership according to that operation's ordinary result type. If a branch is
not selected, the original values and handles remain available according to normal
ownership rules.
This matters especially for message sends, revocable sends and move-only messages. The runtime must not partially consume a message on a branch that loses the select.
Memory Semantics
A committed send/receive pair establishes the channel's required synchronization edge. The receiver observes memory effects published by the sender according to the concurrency memory model. Polling statistics or merely observing cancellation state is not equivalent to successful channel synchronization.
select must preserve the same synchronization semantics as direct send or
receive operations. Only the committed branch contributes the synchronization edge.
Target and Runtime Requirements
The selected immutable CompilationPlan determines whether channels,
select, timeouts and optional capabilities are supported. A target may implement them
through scheduler wait sets, event loops, RTOS primitives, OS polling facilities or
compiler-generated cooperative state machines.
Whatever mechanism is used, the target must preserve the Sec source semantics:
- exact result types;
- in-process channel identity;
- source-order priority in select;
- exactly-one branch commit;
- ownership rollback for non-selected branches;
- cancellation-point behavior for waiting operations;
- timeout and synchronization rules.
A target that cannot support a required branch combination or capability must reject the program instead of approximating the semantics.
Tooling and Diagnostics
Hover, completion and navigation should expose the canonical channel surface rather
than legacy names. Tooling should prefer Tx, Rx,
Share, Send, TrySend, Receive,
TryReceive, SendRevocable, Revoke,
Discard and Close.
Common diagnostics include:
- use of consumed sender or receiver capability;
- attempting to share beyond
MaxSenders; - using revocation APIs without the
Revocationcapability; - using statistics without the
Statisticscapability; - placing
defaultbefore the final branch inselect; - combining a positive timeout with a final
defaultthat makes the timeout unreachable; - using timeouts on a target without a valid timer source.
Implementation Status
The channels rulebook is normative and the governance status for the integrated channel surface is partial.
Already present or partly present:
- compiler-known intrinsic identities for
Channel[T],Sender[T],Receiver[T]andMessageTicket[T]; - initial compiler-known support for several related result/configuration identities.
Still incomplete or planned:
- the full exact source API and all capability-gated operations;
- complete lifecycle semantics for close/discard/revocation/expiration;
- select integration with exact readiness/commit rollback behavior;
- cancellation-point commit races and target/runtime lowering;
- complete Semantic IR and LSP/tooling support.
Best Practices
- Use channels for explicit ownership transfer between concurrent entities.
- Model sender fan-out explicitly with
Share()and a deliberateMaxSenders. - Use blocking operations when waiting is acceptable and
Try...operations when it is not. - Handle returned ownership explicitly on
Closed(T)andWouldBlock(T). - Use revocable sends only when you truly need post-send recovery behavior.
- Use timeout composition through
selectandafter; do not invent timed wrappers in user code. - Treat source order in
selectas a real priority policy. - Do not assume fairness for later select branches.
- Keep ordinary channels in-process; use the IPC APIs for cross-process messaging.
- Remember that a statistics snapshot is observability, not program synchronization.
Summary
Channel[T]is the canonical typed in-process message channel in Sec 0.1.Channel[T]is@noCopyand exposes exactlyTxandRx.Sender[T],Receiver[T]andMessageTicket[T]are move-only capabilities.- Construction is fallible only through
AllocationErrorwhen needed. ChannelOptionsfixes capacity, sender limit and capability set.- The optional capabilities are exactly Revocation, Expiration, ISRSafeSend and Statistics.
- Blocking
Send(...)usesChannelSendResult[T]; non-blockingTrySend(...)usesChannelTrySendResult[T]. - Blocking
Receive()returnsOption[T]; non-blockingTryReceive()usesChannelTryReceiveResult[T]. Share()returnsOption[Sender[T]]and respectsMaxSenders.- Revocable sends use
MessageTicket[T]andChannelRevokeResult[T]. - Ordinary channels are FIFO by committed message order and strictly in-process.
- Waiting channel operations are cancellation points;
Try...operations are not. - Ordinary channels define no timed APIs and no
ref Contextoverloads. selectwaits on multiple concurrent operations and commits exactly one branch.- Source order is the explicit priority rule in
select. defaultis optional, must be final, and makes the select non-waiting.afterprovides timeout composition using a monotonic time source.- Non-selected select branches must not consume ownership or mutate observable state.
- The normative model is complete, but current implementation support remains partial.