Channel
In the prelude: every iyi program has it, with no import.
T : Share is not enforced yet for the reason the file header gives: one thread cannot race, and a marker that gates nothing cannot be tested for gating. select is answered by this protocol — a node per arm, one nonce, losers stale by arithmetic until the woken select takes them out — at the bottom of the class.
Channel(T)class
class Channel(T)
# Everyone parked is woken; what each wake means is read on the woken
# side — a receiver finds the buffer, then `ChannelClosed`; a sender
# finds its box still full.
def close : Nil
def closed? : Bool
def new(capacity : Int32)
def new
# `select`'s arms park through these with one shared nonce; a plain
# call parks with a fresh nonce of its own. *slot* is where a rendezvous
# sender puts its value (`IyiWaiter#slot`).
def park_receive_node(fiber : IyiFiber, nonce : UInt64, slot : UInt64) : IyiWaiter
def park_send_node(fiber : IyiFiber, nonce : UInt64, value : T) : IyiSendNode(T)
# Assumes `send_ready?`. A rendezvous puts the value in the parked
# receiver's own slot (`hand`). It went into the buffer, and the send
# answered delivered on the word of the wake: a select woken that way
# took another arm ready by then, a receiver cancelled before it ran
# answered `Cancelled`, and either way the value stayed in a channel of
# no capacity after its sender left. A buffered send completes when the
# buffer takes it, so there the woken receiver finds it in the buffer.
def put_now(value : T) : Nil | ChannelClosed
# A closed channel drains what was delivered before it refuses. A
# rendezvous sender that finds this receive parked writes its value into
# `got` and marks the node handed, and that is the answer even when the
# task was cancelled before it ran: the sender was told it was taken.
# A woken park that was handed nothing takes its node out of the queue,
# so a cancelled one leaves nothing behind.
def receive : T | ChannelClosed | Cancelled
def receive_ready? : Bool
# Nothing happens on the arm `take_now` answers: `ChannelClosed` is
# the refusal here, where a closed channel's buffer is spent.
def receive_refused? : Bool
def receive_select_action : IyiReceiveSelect(T)
def send(value : T) : Nil | ChannelClosed | Cancelled
def send_ready? : Bool
def send_select_action(value : T) : IyiSendSelect(T)
# Assumes `receive_ready?`: the buffer, then a parked sender's box,
# then the refusal.
def take_now : T | ChannelClosed
# Out of the receive queue from wherever it stands, in constant time,
# and no longer linked anywhere; a node no queue holds is left as it
# is. The queue is linked both ways because a select's losing arms
# leave from the middle: they stayed until something walked them off
# the head, so a loop selecting over a busy channel and an idle one
# left a node in the idle one's queue for every park - 30 MiB kept
# after two million rounds, freed only when the idle channel was used.
def unlink_receiver(node : IyiWaiter) : Nil
# The send queue's `unlink_receiver`.
def unlink_sender(node : IyiSendNode(T)) : Nil
# No arm ready answers -1, which the expansion's case meets with the
# program's own `else`.
def self.non_blocking_select(actions : U, bound : Int32) forall U
# ── select — III.4.6, the library half of the expansion ────────────
#
# A `select` compiles to `%i, %v, %h = ::Channel.select({arm, arm}, b)`
# and a case over the index; `non_blocking_select` when an `else` arm
# exists. One def serves every arity, because a tuple's size is a
# compile-time fact a macro walks. `%h` says the arm's operation
# happened, and an arm nobody binds runs its body only then: it ran
# unless its answer was an `Error`, so a `Channel(Boom)`'s receive took
# the `Boom` and skipped the body. *bound* is the first arm that binds,
# -1 for none, and a cancelled task's `Cancelled` goes to it, as that
# arm's own primitive would have answered it.
def self.select(actions : U, bound : Int32) forall U
end
Channel(T).newis a rendezvous, as Crystal's is: a send completes when a receive takes it, and either side parks until the other arrives. A parked sender carries its value in its node's box, and a parked receiver has a slot a sender fills, so the handoff needs no third party;new(capacity)buffers that many sends first. Closing drains what was delivered, then refuses: a parked sender whose box is still full failed, and its value dies with the node.