std::net — the Socket std Lends, and the First Untrusted SourceStatus: Superseded by ADR-289 — its D1-D5 are D7-D11 there
Date: September 22, 2026
Target Version: Nikaia 0.0.164
Builds: ADR-194 D1 — the socket is std’s and the protocol
is the package’s
Related: ADR-069 D2 (the subtraction this is carried one item
further), ADR-121 D4 (the awaitable readiness it waits on),
ADR-010 D2 (the provenance it is the first to carry),
ADR-033 D3 (the resource kind it adds),
ADR-006 (the Cleanup it does not wait for)
ADR-194 D1 decided where the socket lives and said nothing about
what it looks like. std::net was one line in Part III 17.1 — low-level
TCP/UDP sockets for building custom protocols — and nothing behind it.
Three records wait on something for a handler to run for: ADR-018 entire, ADR-058’s body rows, and the roadmap’s route hashing. #90 calls this step the blocker.
net::listen(address) -> Listener // bind and listen
net::connect(address) -> Connection // the other end, for a client
Listener.accept() -> Connection // the next one, waiting
Listener.address() -> String // what port 0 turned into
Connection.read() -> Bytes // what the peer sent
Connection.write(bytes) // all of it
Connection.peer() -> String // who
Connection.close() // now rather than at the drop
read hands back Bytes, which is ADR-156 D1’s one shared
buffer and what fs::read already does. It costs an allocation per read, and
the alternative — a buffer the caller reuses — is a second shape with a lifetime
story of its own: what comes back here is a value a program may keep, so it
cannot be a window into what the next read overwrites.
write takes all of it. A partial write is not something a caller can do
anything useful with; the loop that would follow one is inside this.
An empty read is the peer closing. That is what a zero-length read has meant on every socket there has ever been, and it is the one thing a caller has to know to write a loop.
std has.ADR-010 D2’s column has had nothing to fire on: every source std
carries is the operator’s own — a file they named, a pipe they connected, the
arguments they typed. std.contracts’ own header predicted this one by name:
the untrusted ones — http, net, db — arrive with the modules that have
them.
net::Connection::read carries provenance = "untrusted", and the analysis
that reads it was built long before anything wrote it.
touches gains socket, and it names no socket.ADR-033 D3 named it in the same breath as a file — a file, a
socket, stdout and a Locked value are not in the set of things that can be
pointed at — and the kind waited under the rule lock waited under: a word
waits until a program can ask for it. One can now.
It names no parameter, for lock’s reason rather than for want of a name: a
listener could be named by the address it is bound to, and a connection is
what a program actually touches. Telling two connections apart is the alias
analysis ADR-039 D4 refuses to make a program’s compilation depend
on. So two reads of two sockets do not conflict — reads never do — and a write
orders against every socket touch, which is the safe direction and the one a
server’s two writes want anyway.
accept, read and write are non-blocking underneath; where the kernel says
not yet, they ask ADR-121 D4’s rt::io::waiting for readiness
and await it. The same park fs uses and the same one a task uses, so one
thread serves many connections at user_parallelism = no exactly as at yes —
which crates/nikaia/tests/sockets.rs asserts by running the same program at
both settings and comparing what it printed.
And ADR-038 D3’s rule holds where it is hardest to hold: nothing
in a .nika file that binds a socket says async, await, epoll, io_uring
or poll. Every other language makes a reader choose a runtime before they can
bind one.
Two calls do not, and it is stated rather than hidden. bind is a syscall
that answers immediately. connect is not, and this one blocks on it: what
the MVP connects to is a listener on the same machine
(ADR-194 D5), and a non-blocking connect is a second readiness
shape for a case nothing here has yet.
Cleanup is not waited for.Dropping a Connection closes it, which is the language below’s own answer
rather than a decision this makes. A program that wants the close at a named
moment writes close. ADR-006’s language-level cleanup is unbuilt,
and a socket is not the thing to build it for.
nikaia serve [dir].net is in the prelude, for fs’s reason: a module a program reaches
through its prefix has to be in scope in the generated file, and nothing a
program writes says where it comes from.Connection and Listener may cross a thread, so a handler on a pool may
be handed one. Both are a file descriptor and an address and nothing else.NK1178’s untrusted half can fire on exists now, and
nothing had to change to make it.rustls and its own step.read and not a change to it.rt::io::waiting takes one and nothing here
passes it; what a server does about a slow peer is the server’s
(ADR-194 D5’s caps).net does not exist there.All of it. crates/nikaia-std/src/net.rs, the nine entries and two types in
std.contracts, socket in contracts::touch::KINDS, and two test binaries:
crates/nikaia-std/tests/sockets.rs for the module and
crates/nikaia/tests/sockets.rs for a .nika program that binds, accepts,
reads and writes — at both settings of user_parallelism, with the same output
from each.