ADR-198: std::net — the Socket std Lends, and the First Untrusted Source

Status: 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)

1. Context

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.

2. Decisions

D1 — Four verbs and two types, and nothing else.

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.

D2 — What comes off a socket is untrusted, and it is the first one 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.

D3 — 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.

D4 — Everything that waits gives the thread up, and the program cannot tell.

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.

D5 — Closing is the drop, and a 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.

3. Consequences

4. What this does not decide

5. What is built

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.

This site is open source. Improve this page.