network: UnixListener and UnixStream in std/network (step 2 of #119) #546

Closed
opened 2026-10-28 14:24:37 +00:00 by orion · 2 comments
Collaborator

Goal

Second step of #119 (Unix domain sockets for std/network, FIRST-PARTY.md gap 21): a program can listen on and connect to a Unix stream socket by path, with the same shape as the TCP types. The runtime side landed in step 1 (#428, PR #430, commit 5dd2b9a4): the natives networkListenUnix and networkConnectUnix, the sandbox refusal, and the notes in docs/design/NETWORK.md ("Unix domain sockets, as built (step 1)").

Design

Decided in #119 (body and comment decided by the loop); docs/design/NETWORK.md section 10 for the sandbox. New file std/network/src/unix.trb, exported from std/network/src/lib.trb (public use UnixListener, UnixStream, UnixSource, UnixSink from "./unix"). Stream sockets only. Methods, no settings object, a path is a String (no std/path dependency). Mirror the TCP types in std/network/src/tcp.trb:

  • public shared type UnixListener with Close: static fn listen(path: String, backlog: Int = 128): Result<UnixListener, NetworkError>, fn path(): String, fn accept(): Task<Result<UnixStream, NetworkError>>, fn stop() and var fn close() like TcpListener. Closing the handle removes the socket file the runtime bound (done by the native, nothing to do in std/). No acceptor() in this step (a Unix server on every core is a later addition, filed when needed).
  • public shared type UnixStream with Close: static fn connect(path: String): Task<Result<UnixStream, NetworkError>>, fn receive(maximum: Int = 65536): Task<Result<Bytes?, NetworkError>>, fn send(bytes: Bytes): Task<Result<Void, NetworkError>>, fn shutdown(): Result<Void, NetworkError>, fn source(chunk: Int = 65536): UnixSource, fn sink(): UnixSink, var fn close(). UnixSource is a Source<Bytes, NetworkError>, UnixSink a Sink<Bytes, NetworkError> (end() is a half close), as TcpSource and TcpSink.
  • No socket options and no localAddress/peerAddress (an accepted stream has no peer path; the options of a TCP stream are invalid argument on a Unix one).
  • Errors are the existing NetworkError; no new kind. NetworkError.of(code).about("listening on {path}") as TCP does.
  • Natives: the two new ones are native fn declarations in unix.trb (networkListenUnix(path: String, backlog: Int): Int, networkConnectUnix(path: String): Task<Int>), matching the manifest entries of step 1 (runtime/include/torb_network.h). accept, receive, takeReceived, send and shutdown use the existing natives on the new handles: they are made public native fn in tcp.trb the way networkClose already is (imported by unix.trb, not exported by the package), or declared again in unix.trb if the compiler allows it and it is smaller; the implementer picks, it is internal.
  • No shared stream trait (gap 17 owns the transport question): two small mirrors.
  • A sandbox with no grant stops the script at the native (TORB_SANDBOX_REFUSED, step 1); nothing in std/ changes for it. A unix: pattern is step 3 of #119, not this one.

Acceptance criteria

  • UnixListener, UnixStream, UnixSource and UnixSink exist in std/network/src/unix.trb with doc comments and are exported from std/network.
  • Tests in std/network/tests (a new unix.test.trb) over a socket file in a directory made with File.createTemporaryDirectory (removed at the end): a listener bound to a path accepts a UnixStream.connect to it; bytes go both ways (send/receive, and through source()/sink()); shutdown() gives the peer None from receive (end of stream) while the other direction still works; connect to a path nobody bound fails with isUnreachable() (the refused case, a file that exists with nobody listening, is asserted only if it needs no more than a plain file at the path); listen on a path that exists fails with isAddressInUse(); path() answers the path given; after the listener closed the file is gone (File.exists false); a path longer than sun_path fails with an invalid-argument NetworkError. Run natively and in the VM (the existing std/network test setup, like options.test.trb: the scenarios run at the top of the file where the file's own task can wait).
  • docs/standard-library/network.md documents the four types; docs/design/NETWORK.md section 4 and the slice table say Unix sockets are done (step 2) and what stays later (datagrams, abstract names, peer credentials, descriptor passing, the unix: grant); docs/design/FIRST-PARTY.md gap 21 marked done with the date and a pointer to std/network/src/unix.trb and the tests (the "Unix sockets" mention under torb/postgres stays that package's); the generated skills/ regenerated (docs change, docs skill docs skills).
  • Tier A green in forge CI (sh tools/gates.sh a); b if CLAUDE.md asks for it for the touched paths. Windows and macOS code of step 1 was not run by CI; this step adds no per-system code, so no new claim about them.

Seed

Touches the seed: no further seed change. This is the second commit of the two-commit rule of CLAUDE.md ("Seed and breaking changes"): the natives were taught in step 1 (#430, 5dd2b9a4, 2026-10-23), and the published seeds (release seeds; newest 142293fd98fc, built from a commit after it, and c4a8443532aa of 2026-10-24) contain them, so std/network can use them and the compiler (which imports std/network in compiler/src/debugger/adapter.trb) still bootstraps in CI. No new native, no syntax change, no new driver flag, no std name looked up by string by the compiler. A seed refresh stays the merge gate's and Torben's.

Size

One PR, one commit: feat(network): UnixListener and UnixStream. About 2 to 3 h: ~250 lines of std/ (a mirror of the TCP types without the options, address and acceptor), one test file, three doc edits and the regenerated skills.

Dependencies

Step 1 (#428, PR #430) landed. Independent of #120 step 2 (#473, PR #477, TcpStream options in std/network/src/tcp.trb): both edit tcp.trb/lib.trb/network.md/NETWORK.md, so whichever lands second rebases. Nothing else.

Source

#119 (epic, design), docs/design/NETWORK.md sections 4 and 10, docs/design/FIRST-PARTY.md gap 21.

## Goal Second step of #119 (Unix domain sockets for `std/network`, FIRST-PARTY.md gap 21): a program can listen on and connect to a Unix stream socket by path, with the same shape as the TCP types. The runtime side landed in step 1 (#428, PR #430, commit `5dd2b9a4`): the natives `networkListenUnix` and `networkConnectUnix`, the sandbox refusal, and the notes in `docs/design/NETWORK.md` ("Unix domain sockets, as built (step 1)"). ## Design Decided in #119 (body and comment `decided by the loop`); `docs/design/NETWORK.md` section 10 for the sandbox. New file `std/network/src/unix.trb`, exported from `std/network/src/lib.trb` (`public use UnixListener, UnixStream, UnixSource, UnixSink from "./unix"`). Stream sockets only. Methods, no settings object, a path is a `String` (no `std/path` dependency). Mirror the TCP types in `std/network/src/tcp.trb`: - `public shared type UnixListener with Close`: `static fn listen(path: String, backlog: Int = 128): Result<UnixListener, NetworkError>`, `fn path(): String`, `fn accept(): Task<Result<UnixStream, NetworkError>>`, `fn stop()` and `var fn close()` like `TcpListener`. Closing the handle removes the socket file the runtime bound (done by the native, nothing to do in `std/`). No `acceptor()` in this step (a Unix server on every core is a later addition, filed when needed). - `public shared type UnixStream with Close`: `static fn connect(path: String): Task<Result<UnixStream, NetworkError>>`, `fn receive(maximum: Int = 65536): Task<Result<Bytes?, NetworkError>>`, `fn send(bytes: Bytes): Task<Result<Void, NetworkError>>`, `fn shutdown(): Result<Void, NetworkError>`, `fn source(chunk: Int = 65536): UnixSource`, `fn sink(): UnixSink`, `var fn close()`. `UnixSource` is a `Source<Bytes, NetworkError>`, `UnixSink` a `Sink<Bytes, NetworkError>` (`end()` is a half close), as `TcpSource` and `TcpSink`. - No socket options and no `localAddress`/`peerAddress` (an accepted stream has no peer path; the options of a TCP stream are `invalid argument` on a Unix one). - Errors are the existing `NetworkError`; no new kind. `NetworkError.of(code).about("listening on {path}")` as TCP does. - Natives: the two new ones are `native fn` declarations in `unix.trb` (`networkListenUnix(path: String, backlog: Int): Int`, `networkConnectUnix(path: String): Task<Int>`), matching the manifest entries of step 1 (`runtime/include/torb_network.h`). `accept`, `receive`, `takeReceived`, `send` and `shutdown` use the existing natives on the new handles: they are made `public native fn` in `tcp.trb` the way `networkClose` already is (imported by `unix.trb`, not exported by the package), or declared again in `unix.trb` if the compiler allows it and it is smaller; the implementer picks, it is internal. - No shared stream trait (gap 17 owns the transport question): two small mirrors. - A sandbox with no grant stops the script at the native (`TORB_SANDBOX_REFUSED`, step 1); nothing in `std/` changes for it. A `unix:` pattern is step 3 of #119, not this one. ## Acceptance criteria - [ ] `UnixListener`, `UnixStream`, `UnixSource` and `UnixSink` exist in `std/network/src/unix.trb` with doc comments and are exported from `std/network`. - [ ] Tests in `std/network/tests` (a new `unix.test.trb`) over a socket file in a directory made with `File.createTemporaryDirectory` (removed at the end): a listener bound to a path accepts a `UnixStream.connect` to it; bytes go both ways (`send`/`receive`, and through `source()`/`sink()`); `shutdown()` gives the peer `None` from `receive` (end of stream) while the other direction still works; `connect` to a path nobody bound fails with `isUnreachable()` (the refused case, a file that exists with nobody listening, is asserted only if it needs no more than a plain file at the path); `listen` on a path that exists fails with `isAddressInUse()`; `path()` answers the path given; after the listener closed the file is gone (`File.exists` false); a path longer than `sun_path` fails with an invalid-argument `NetworkError`. Run natively and in the VM (the existing `std/network` test setup, like `options.test.trb`: the scenarios run at the top of the file where the file's own task can wait). - [ ] `docs/standard-library/network.md` documents the four types; `docs/design/NETWORK.md` section 4 and the slice table say Unix sockets are done (step 2) and what stays later (datagrams, abstract names, peer credentials, descriptor passing, the `unix:` grant); `docs/design/FIRST-PARTY.md` gap 21 marked **done** with the date and a pointer to `std/network/src/unix.trb` and the tests (the "Unix sockets" mention under `torb/postgres` stays that package's); the generated `skills/` regenerated (docs change, `docs skill docs skills`). - [ ] Tier A green in forge CI (`sh tools/gates.sh a`); `b` if `CLAUDE.md` asks for it for the touched paths. Windows and macOS code of step 1 was not run by CI; this step adds no per-system code, so no new claim about them. ## Seed Touches the seed: **no further seed change**. This is the second commit of the two-commit rule of CLAUDE.md ("Seed and breaking changes"): the natives were taught in step 1 (#430, `5dd2b9a4`, 2026-10-23), and the published seeds (release `seeds`; newest `142293fd98fc`, built from a commit after it, and `c4a8443532aa` of 2026-10-24) contain them, so `std/network` can use them and the compiler (which imports `std/network` in `compiler/src/debugger/adapter.trb`) still bootstraps in CI. No new native, no syntax change, no new driver flag, no std name looked up by string by the compiler. A seed refresh stays the merge gate's and Torben's. ## Size One PR, one commit: `feat(network): UnixListener and UnixStream`. About 2 to 3 h: ~250 lines of `std/` (a mirror of the TCP types without the options, address and acceptor), one test file, three doc edits and the regenerated skills. ## Dependencies Step 1 (#428, PR #430) landed. Independent of #120 step 2 (#473, PR #477, `TcpStream` options in `std/network/src/tcp.trb`): both edit `tcp.trb`/`lib.trb`/`network.md`/`NETWORK.md`, so whichever lands second rebases. Nothing else. ## Source [#119](https://git.torb.dev/torbscript/language/issues/119) (epic, design), `docs/design/NETWORK.md` sections 4 and 10, `docs/design/FIRST-PARTY.md` gap 21.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261028 until 2026-10-28T20:27:50Z

[loop:torbscript] claim implement by session:loop-torbscript-20261028 until 2026-10-28T20:27:50Z
Author
Collaborator

[loop:torbscript] implement: PR #548 opened (feat(network): UnixListener and UnixStream, one commit): std/network/src/unix.trb, exports, unix.test.trb (12 tests pass in the VM locally on the nightly toolchain), docs and regenerated skills; format and docs checks clean locally, tier A runs in CI. Worker usage: one Sonnet 5.5 sub-agent session, tokens not metered here. next: waiting (CI on PR and merge gate)

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/548 opened (`feat(network): UnixListener and UnixStream`, one commit): `std/network/src/unix.trb`, exports, `unix.test.trb` (12 tests pass in the VM locally on the nightly toolchain), docs and regenerated skills; format and docs checks clean locally, tier A runs in CI. Worker usage: one Sonnet 5.5 sub-agent session, tokens not metered here. next: waiting (CI on PR and merge gate)
argus closed this issue 2026-10-28 22:16:23 +00:00
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#546
No description provided.