# `Hyperlang.Chan`
[🔗](https://github.com/hyperbob-labs/hyperlang/blob/v0.1.0/lib/hyperlang/chan.ex#L1)

Typed channels and `alt` for syscall hosts.

These are the Dis channel primitives that a ported Limbo userland needs,
with the semantics of the Inferno interpreter. Guests reach them through
the `sys:*` facade (`newchan`, `csend`, `crecv`, `alt`). Hosts call this
module directly to create, alias and close channels on a guest's behalf.

## What this ports

This is the BEAM home of the Dis channel primitives from `libinterp/xec.c`
(`isend`, `irecv`, `cqadd`/`cqdel`, `cgetb`/`cputb`) and `libinterp/alt.c`
(`altrdy`/`altcomm`/`altdone`). In Dis those ops run inside the single-threaded
interpreter loop, so a channel operation is *atomic* w.r.t. every other Prog: a
send either hands its value to a waiting receiver and wakes it, or blocks the
sender on the channel's `send` queue; a receive is symmetric; `alt` commits
exactly one ready op atomically.

On the BEAM each Inferno `Prog` is its own BEAM process (a hyperlang guest
worker), so the atomicity that Dis got from a single interpreter thread is
recovered by making each **channel a serialising OTP process** (the broker).
The broker pid is the C `Channel*`. Every `csend`/`crecv`/`alt`/`nbsend`/
`nbrecv` is one `GenServer.call` into the broker, so all the rendezvous
bookkeeping (which Prog is parked on `send`/`recv`, the buffer ring) happens
one message at a time — exactly the Dis invariant. The *calling* Prog's worker
process is what blocks (it waits for the broker's reply), never a busy-wait.

### Rendezvous (unbuffered, `size == 0`)

`isend`: if a receiver is already parked (`c->recv->member != nil`) hand the value
over directly and wake it; else park the sender on `c->send` and block. `irecv`
is the mirror. Exactly one value is handed off per matched pair — no loss, no
double-take — because the broker matches under its single-process serialisation.

### Buffered (`size > 0`)

A ring buffer of `size` slots (`cputb`/`cgetb`). Send blocks only when the
buffer is full AND no receiver waits; recv blocks only when the buffer is empty
AND no sender waits. A waiting receiver still takes precedence over the buffer
for a fresh send (matching `isend`: it checks `c->recv->member` *before* `cputb`).

### alt

`alt(ops)` is Dis `altrdy`+`altcomm`+`altdone`: scan the ops, collect the set
that can fire right now; if any are ready, pick one **fairly** (uniformly at
random over the ready set, the BEAM stand-in for Dis's run-queue order) and
perform exactly that communication. If none are ready and there is a `*`
default, take it without blocking; otherwise the caller blocks until one op
becomes ready, then commits exactly one. `nbsend`/`nbrecv` are `alt` with a
single op plus a default.

## Handles cross the guest boundary as integers, not pids

A broker pid cannot cross the hyperlang term boundary — the sanitizer rejects
pids, ports and refs as forbidden terms. So,
exactly like a file descriptor, a channel is handed to the guest as an **opaque
non-negative integer handle**. The handle → broker-pid map lives in a registry
keyed by the caller's opaque channel scope. The OS installs the
`:guest_channel_scope` hook when it wants guest-facing operations to resolve
handles; kernel clients can still pass raw broker pids directly.

## What is DROPPED (Dis/BEAM already provides it)

  * The Dis heap `Channel` type, `H2D`/`D2H` tagged-pointer plumbing, the
    `mover`/`mid`/`Type` copy machinery, and `freechan` — BEAM terms are copied
    by message-passing and reclaimed by GC.
  * `Tarray`-of-channels alt arms (`==>` over an array of channels); `alt`
    here takes a flat op list.

# `alias_handle`

Install `handle` in `to_scope` when it is a live channel capability in
`from_scope`.

Guest channel messages can carry channel handles created after `pctl(FORKNS)`.
The handle is still the same Inferno heap-channel capability, but the receiving
Prog may be in a different namespace scope. Aliasing only from a scope that
already owns the handle preserves the capability boundary while letting the
handle cross explicitly in a channel payload.

# `alt`

Non-deterministic select over `ops`. See module doc for the op/return shapes.

# `alt_raw`

Non-deterministic select without stripping the channel payload transport envelope.

# `broker?`

True when `pid` is one of this module's channel broker processes.

# `close_broker`

Close an ephemeral broker and remove every registry handle that points at it.

This is the BEAM counterpart of devsrv's `destroy(rc/wc)` for per-request
Rread/Rwrite reply channels after the kernel receives the reply.

# `close_scope`

Release the channel set owned by `scope`. Drops the scope's handle->broker
registry entries and TERMINATES every broker that is no longer referenced by
any other scope (a FORKNS-shared broker survives until its last scope closes).
Without this a closed lifetime leaks both
the broker process (under `BrokerSupervisor`) and a stale registry entry that
still resolves. A no-op if the registry is not started.

# `cnewc`

`cnewc(t, mover, len)` (libinterp/xec.c) — allocate a fresh typed channel with
`len` buffer slots (`len > 0` = buffered, as devsrv uses for its reply/request
channels). `t` is the Dis value `Type*` (here the type TAG `:rread`/`:rwrite`/
`:fileio_read`/`:fileio_write` — the heap GC type is DROPPED, only the tag is
carried); `mover` is `movtmp()`. Returns the broker PID = the C `Channel*`.

Unlike `newchan/2` this does NOT install the channel in a scope handle registry:
these are kernel-internal `Channel*`s held by devsrv directly, never crossing
the guest term boundary.

# `crecv`

Receive a value from channel broker `c`, blocking until one is available.
Returns the value.

# `crecv_raw`

Receive a value without stripping the channel payload transport envelope.

This is only for the guest boundary, which must inspect the source scope before
exposing nested channel handles back to a guest.

# `csend`

Send `v` on channel broker `c`, blocking (rendezvous / buffer-full) until the
value is taken or buffered. Returns `:ok`.

# `csendalt`

`csendalt(c, value, len)` (libinterp/xec.c) — a NON-BLOCKING send used by
devsrv's `srvunblock` to push a sentinel request onto a server's read/write
channel without parking. In Dis it grows the channel buffer if needed
(`heaparray(t, len)`) so the `isend` it performs never blocks. Faithful
mapping: hand the value to a parked receiver / armed alt-recv waiter if one is
ready, else buffer it (growing past `size` as needed — the Dis buffer-grow).
Always succeeds (returns 1, as the C returns 1 on the buffered path); `c` is
the broker PID, `len` the requested buffer length (`-1` = grow on demand).

# `current_scope`

Resolve the current guest channel scope through the `:guest_channel_scope`
app-env hook.

The hook may be a zero-arity function or `{module, function}`. Missing or bad
hooks fail closed with `{:error, :no_scope}`.

# `ensure_started`

Start the channel registry and broker supervisor (idempotent).

This runtime starts on first use, so channel operations do not require the
OTP application tree to have been booted.

# `fork_scope`

`pctl(FORKNS)` channel carry-over. Inferno channels are heap objects shared
across a proc and its children regardless of the namespace group, and FORKNS
reshapes only the namespace (`Pgrp.scopecpy`), never the channel set. So when
the namespace scope is replaced by a fresh one, re-key the forking proc's live
channel handles from `old_scope` into `new_scope` (same handle ints, same broker
pids) so its existing handles still resolve in the child. A no-op if either
scope is nil or the registry is not started.

# `freechan`

Free a channel alias, revoking it and releasing its scope quota before return.
A broker shared by another scope remains alive. The last alias schedules
retirement in the broker itself, so the allocator never waits on broker
supervision or a close hook. The hook runs once when retirement completes.
Scope closure still waits for that lifetime's orphaned brokers directly.

# `guest_channel_value`

# `handle_for`

Expose an existing channel broker through `scope`'s integer-handle registry.

Used when a kernel-internal FileIO request carries a reply `Channel*` to guest
code: the kernel keeps the broker pid, while the guest receives a scoped int
handle and replies through the normal sys:send/alt path.

# `link_fileio_pair`

Pair the read/write brokers of one `Sys->FileIO`.

Inferno's `freeFileIO` destroys both channel fields together. The BEAM port
uses integer scope handles for the guest view, so the channel brokers need an
explicit peer link: when the server Proc that first receives from either side
exits, the broker tears down its peer too, causing both devsrv close hooks to
run.

# `movtmp`

The `movtmp` mover sentinel (libinterp/xec.c `movtmp`). In Dis the channel
`mover` is a function pointer driving the typed value copy during send/recv;
on the BEAM message passing already copies the term by value, so the mover is
an opaque marker passed to `cnewc/3` (kept 1:1 so `cnewc(t, movtmp(), n)`
reads as in the reference). DROPPED: the `memmove`/`freeptrs`/`incmem` copy.

# `nbrecv`

Non-blocking recv: `alt` with one recv op and a default. {:ok, v} | :empty.

# `nbsend`

Non-blocking send: `alt` with one send op and a default. true if sent.

# `newchan`

Create a channel of `size` buffer slots (0 = unbuffered/rendezvous) in process
`scope`. Returns `{handle, broker_pid}`; the handle is the opaque integer the
guest holds, the pid is the broker.

# `newchan`

# `newchan`

# `resolve`

Resolve a channel handle to its broker pid within `scope`, or nil.

# `retype`

`retype(c, hook)` — the BEAM home of devsrv's `srvretype` heap-type swap
(libinterp `freetype(h->t); h->t = t; t->ref++; c->aux = f`). In Dis the swap
installs the close-hook (`Trdchan`/`Twrchan`'s `free` callback =
`freerdchan`/`freewrchan`) and binds `c->aux = f` (the `SrvFile*`). The Dis
heap type / refcount is DROPPED (BEAM GC); the surviving behaviour is the
close-hook binding: `hook` is `{module, fun, aux}`, invoked as
`module.fun(aux, swept)` when the broker is collected (its owning process
exits → the broker's `terminate/2`). `c` is the broker PID.

# `start_registry`

Same as `ensure_started/0`.

# `with_broker`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
