Module net/tcp
Async TCP networking — typed TcpListener and TcpStream objects over
std/sys/tcp, with Exception-based error handling.
Every operation that touches the network is an io.async future, so it
runs on the ONE event-loop thread: awaiting a read parks this task and
lets every other task on the loop run, and nothing here blocks a thread.
Kernel failures are thrown as IoExn carrying an IoError (D1) instead
of being returned, which is why a resolved byte count is always a real
count and never a negative errno.
{ TcpListener, TcpStream } :: import("std/net/tcp");
{ SocketAddr } :: import("std/net/addr");
{ Exception, IoExn } :: import("std/error");
main :: (fn(io : Io, exn : Exception) -> unit)({
e := IoExn(io : io, exn : exn);
listener := io.await(TcpListener.bind(SocketAddr.loopback(u16(8080)), io), e);
stream := io.await(listener.accept(io), e);
io.await(stream.write_str("pong", io), e);
});
Stability
unstable — ONE shape is still open, in the READ direction. A TcpStream
can be written from safe code (write_str, write_string, write_bytes)
but can only be read through read(buf : *(u8), size, io), so a program
without pragma(Pragma.AllowUnsafe) has to go through the Reader
defaults or std/io/bufio's BufReader to get a byte out — the same
asymmetry std/net/udp closed on 2026-09-09 with recv_bytes, and not
yet closed here. Freezing waits on that pointer-free read; the
listener/stream names, which mirror Rust's, are not expected to move.
The second open shape is CLOSED: incoming exists (see Incoming below),
built on the Stream trait in std/async/stream.yo, so a server loop can
be a stream with combinators rather than a hand-rolled accept loop.
Types
Which half of a TCP connection to shut down — the argument to
TcpStream.shutdown.
Variants
| Variant | Fields | Description |
|---|---|---|
Read | Further receives are disallowed (SHUT_RD). | |
Write | Further sends are disallowed (SHUT_WR). | |
Both | Both further sends and receives are disallowed (SHUT_RDWR). |
The stream of connections arriving at a TcpListener — Rust's
TcpListener::incoming, created by listener.incoming().
Item is a Result, not a bare TcpStream, for the reason Rust's is: a
single failed accept (a client that vanished between the SYN and the
accept, a per-process descriptor limit) is ONE bad connection, not the end
of the listener. Carrying the failure in the item is what lets the stream
survive it — and it is why Stream.next needs no Exception handler
(std/async/stream.yo).
The error is a NetError, the same type accept THROWS, so moving a
server loop from accept to incoming does not change its error
vocabulary.
The stream OWNS the listener (incoming moves it in, as tokio-stream's
TcpListenerStream::new does): dropping the stream closes the socket.
listener() lends it back, for local_addr or an early close. The
stream finishes (.None) once the listener is closed.
Fields
| Name | Type | Description |
|---|---|---|
_listener | TcpListener |
Trait Implementations
impl(generic(S : Type), where(S <: Stream), S : (Stream))
map : fn(generic(A, B, F) self : S : (Stream), f : F : (Fn(A) -> B)) -> StreamMap(S : (Stream), B, F : (Fn(A) -> B))filter : fn(generic(A, F) self : S : (Stream), f : F : (Fn(A) -> bool)) -> StreamFilter(S : (Stream), F : (Fn(A) -> bool))Parameters
| Name | Type | Notes |
|---|---|---|
self | S : (Stream) | |
f | F : (Fn(A) -> bool) |
Returns: StreamFilter(S : (Stream), F : (Fn(A) -> bool))
filter_map : fn(generic(A, B, F) self : S : (Stream), f : F : (Fn(A) -> Option(B))) -> StreamFilterMap(S : (Stream), B, F : (Fn(A) -> Option(B)))Parameters
| Name | Type | Notes |
|---|---|---|
self | S : (Stream) | |
f | F : (Fn(A) -> Option(B)) |
Returns: StreamFilterMap(S : (Stream), B, F : (Fn(A) -> Option(B)))
take : fn(generic(A) self : S : (Stream), n : usize) -> StreamTake(S : (Stream))skip : fn(generic(A) self : S : (Stream), n : usize) -> StreamSkip(S : (Stream))for_each : fn(generic(A, F) self : S : (Stream), f : F : (Fn(A) -> unit), io : Io) -> Impl(Future(unit, Io))impl(Incoming, ...)
listener : (Incoming) fn(self : &Incoming) -> &TcpListenerimpl(Incoming, Stream(...))
A TCP socket that listens for incoming connections.
A move-only value, as TcpStream is: one owner closes the socket, and a
copy is E0901. Lend it with &listener; to serve from a spawned task,
move the listener into the task, or share it as Rc(TcpListener).
Fields
| Name | Type | Description |
|---|---|---|
_cell | Box(_TcpListenerState) |
impl(TcpListener, ...)
bind : (TcpListener) fn(addr : SocketAddr, io : Io) -> Impl(Future(TcpListener, IoExn))Bind to a socket address and start listening.
Parameters
| Name | Type | Notes |
|---|---|---|
addr | SocketAddr | |
io | Io |
Returns: Impl(Future(TcpListener, IoExn))
accept : (TcpListener) fn(self : &TcpListener, io : Io) -> Impl(Future(TcpStream, IoExn))Accept an incoming connection, returning a new TcpStream.
The future borrows the listener until it is awaited: the descriptor is read when the accept is submitted, so the listener cannot be dropped (closing it, and freeing the number for reuse) in between.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpListener | |
io | Io |
incoming : (TcpListener) fn(self : TcpListener) -> IncomingThe connections arriving at this listener, as a Stream of
Result(TcpStream, NetError) — Rust's TcpListener::incoming.
conns := listener.incoming().take(usize(3));
io.await(
conns.for_each(c => {
handle(c);
}, io),
io
);
Every std/async/stream combinator applies, and a function can be
generic over where(S <: Stream) instead of over this listener.
The stream takes the listener (it is moved in): read its address first,
or through Incoming.listener().
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpListener |
Returns: Incoming
local_addr : (TcpListener) fn(self : &TcpListener) -> SocketAddrGet the local address this listener is bound to.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpListener |
Returns: SocketAddr
close : (TcpListener) fn(self : &TcpListener, io : Io) -> Impl(Future(unit, IoExn))Close the listener socket.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpListener | |
io | Io |
Returns: Impl(Future(unit, IoExn))
fd : (TcpListener) fn(self : &TcpListener) -> i32A connected TCP stream for bidirectional data transfer.
A move-only value (its state is a Box): moving it moves the socket, and a
copy is E0901. Lend it with &s, or share one stream as Rc(TcpStream)
on one thread or Arc(Mutex(TcpStream)) across threads. Every method
takes self : &Self; close marks the socket closed inside the box, so a
TcpStream is never Sync.
Fields
| Name | Type | Description |
|---|---|---|
_cell | Box(_TcpStreamState) |
Trait Implementations
impl(TcpStream, ...)
connect : (TcpStream) fn(addr : SocketAddr, io : Io) -> Impl(Future(TcpStream, IoExn))Connect to a remote address, returning a new TcpStream.
Parameters
| Name | Type | Notes |
|---|---|---|
addr | SocketAddr | |
io | Io |
write_str : (TcpStream) fn(self : &TcpStream, data : str, io : Io) -> Impl(Future(usize, IoExn))write_string : (TcpStream) fn(self : &TcpStream, data : &String, io : Io) -> Impl(Future(usize, IoExn))write_bytes : (TcpStream) fn(self : &TcpStream, data : &ArrayList(u8), io : Io) -> Impl(Future(usize, IoExn))shutdown : (TcpStream) fn(self : &TcpStream, how : Shutdown, io : Io) -> Impl(Future(unit, IoExn))close : (TcpStream) fn(self : &TcpStream, io : Io) -> Impl(Future(unit, IoExn))close_sync : (TcpStream) fn(self : &TcpStream) -> unitClose the socket now, without the event loop: what Drop runs, for
a caller that must release the descriptor from plain code before the
stream itself goes away (a TLS stream's teardown, a connection pool's
eviction). A no-op once the stream is closed. The destructor itself is
never called by hand (E0915).
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpStream |
Returns: unit
peer_addr : (TcpStream) fn(self : &TcpStream) -> SocketAddrlocal_addr : (TcpStream) fn(self : &TcpStream) -> SocketAddrGet this end's socket address — Rust's TcpStream::local_addr. Read
from the kernel once, when the stream was connected or accepted, because
that is the only way to learn the ephemeral port and the source
interface the kernel chose; neither is in the connect argument.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpStream |
Returns: SocketAddr
fd : (TcpStream) fn(self : &TcpStream) -> i32set_nodelay : (TcpStream) fn(self : &TcpStream, nodelay : bool, io : Io) -> Impl(Future(unit, IoExn))impl(TcpStream, IoTraits)
impl(TcpStream, IoTraits)
Methods
read : (TcpStream) fn(self : &TcpStream, buf : *(u8), size : usize, io : Io) -> Impl(Future(usize, IoExn))Read up to size bytes from the stream into buf, resolving to the
number actually read. 0 — and only 0 — means the peer closed its end;
a SHORT count is ordinary on a stream socket and says nothing about
end-of-stream, so a caller that needs size bytes loops. Awaiting parks
this task on the event loop until the kernel has bytes; a failure throws
IoExn rather than resolving to a negative count.
buf must have room for size bytes, and raw pointers are unavailable
in safe code, so calling this directly needs
pragma(Pragma.AllowUnsafe). Safe code uses the Reader defaults
(read_to_end, read_to_string) or wraps the stream in
BufReader(TcpStream).
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpStream | |
buf | *(u8) | |
size | usize | |
io | Io |
Returns: Impl(Future(usize, IoExn))
read_to_end : (TcpStream) fn(self : &TcpStream, io : Io) -> Impl(Future(ArrayList(u8), IoExn))read_to_string : (TcpStream) fn(self : &TcpStream, io : Io) -> Impl(Future(String, IoExn))write : (TcpStream) fn(self : &TcpStream, buf : *(u8), size : usize, io : Io) -> Impl(Future(usize, IoExn))Write up to size raw bytes from buf, resolving to the number the
kernel accepted — which may be LESS than size. A caller that must
deliver everything loops, or takes the Writer trait's write_all.
Awaiting parks this task until the socket is writable; a failure throws
IoExn. Needs pragma(Pragma.AllowUnsafe) for the raw pointer —
write_bytes / write_str / write_string are the safe spellings.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpStream | |
buf | *(u8) | |
size | usize | |
io | Io |
Returns: Impl(Future(usize, IoExn))
flush : (TcpStream) fn(self : &TcpStream, io : Io) -> Impl(Future(unit, IoExn))Functions
Convert a SocketAddr to a low-level SockAddr buffer.
Shared with std/net/udp, which used to carry a byte-for-byte COPY of this
— which is how the hardcoded-"::1" bug below had to be fixed twice. One
encoder and one decoder (sockaddr_to_socket_addr) for the whole net
layer.
Parameters
| Name | Type | Notes |
|---|---|---|
addr | SocketAddr |
Returns: IO_tcp.SockAddr
Parse a low-level sockaddr buffer (as filled in by accept/getsockname/
recvfrom) back into a SocketAddr. Unknown families fall back to 0.0.0.0:0.
Shared with std/net/udp (its bind readback) — one decoder for the whole
net layer.
Parameters
| Name | Type | Notes |
|---|---|---|
buf | *u8 |
Returns: SocketAddr
_close_stream_state over s's state, for a stream held inside another
value's box (TlsStream): the future captures the state's address and
borrows nothing, so the caller's own future must borrow the owner of s
until this one completes.
Parameters
| Name | Type | Notes |
|---|---|---|
s | &TcpStream | |
io | Io |
Returns: Impl(Future(unit, IoExn))