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 finishes (.None) once the listener is closed — including by
another task while a next is pending — mirroring Watcher.
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, Stream(...))
A TCP socket that listens for incoming connections.
Fields
| Name | Type | Description |
|---|---|---|
_fd | i32 | |
_local_addr | SocketAddr | |
_is_closed | bool |
Trait Implementations
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.
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.
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) -> i32impl(TcpListener, Dispose(...))
dispose : (TcpListener) fn(self : TcpListener) -> unitRelease the resources self owns — a file descriptor, a socket, a lock,
a buffer the allocator handed out. Called automatically when the value's
owner drops it (a value type) or the last reference to it goes away (a
reference type), so an implementor never calls it directly and must
tolerate being the only one who ever does.
It must be safe to run exactly once, and a type that also exposes an
explicit close/release is responsible for making the second call a
no-op.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpListener |
Returns: unit
A connected TCP stream for bidirectional data transfer.
Fields
| Name | Type | Description |
|---|---|---|
_fd | i32 | |
_peer_addr | SocketAddr | |
_local_addr | SocketAddr | |
_is_closed | bool |
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 |
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))
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))
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))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, Dispose(...))
dispose : (TcpStream) fn(self : TcpStream) -> unitRelease the resources self owns — a file descriptor, a socket, a lock,
a buffer the allocator handed out. Called automatically when the value's
owner drops it (a value type) or the last reference to it goes away (a
reference type), so an implementor never calls it directly and must
tolerate being the only one who ever does.
It must be safe to run exactly once, and a type that also exposes an
explicit close/release is responsible for making the second call a
no-op.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpStream |
Returns: unit
impl(TcpStream, IoTraits)
impl(TcpStream, IoTraits)
Methods
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))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