Add net.tcp.connect and return bound port from net.tcp.listen #475

Closed
opened 2026-05-12 01:25:56 +00:00 by navicore · 2 comments
Owner

Summary

net.tcp.* exposes only the server side: listen, accept, read, write, close. There is no way for a Seq program to initiate a TCP connection. UDP got this right (net.udp.send-to); TCP did not.

Two gaps, both small:

1. Missing net.tcp.connect

Proposed signature, symmetric with net.udp.send-to host/port shape:

net.tcp.connect  ( a String Int -- a Socket Bool )
                   host  port      socket success

On success the returned Socket is interchangeable with one returned by net.tcp.accept — same read / write / close semantics. On failure: (0, false) matching the existing TCP convention.

2. net.tcp.listen should return the bound port

Current:

net.tcp.listen  ( a Int -- a Socket Bool )
                  port    socket success

Compare UDP, which already does the right thing:

net.udp.bind    ( a Int -- a Socket Int Bool )
                  port    socket bound success

Requesting port = 0 lets the OS pick an ephemeral port, but with the current TCP signature there is no way to discover what port was assigned. This makes loopback self-tests effectively impossible without hard-coding a port (and racing).

Proposed:

net.tcp.listen  ( a Int -- a Socket Int Bool )
                  port    socket bound success

For non-zero port, bound == port. For port == 0, bound is the OS-assigned port. Matches the UDP convention exactly.

Motivation

Seqlings chapter 26 ("TCP") currently has five exercises that all amount to # I AM NOT DONE + true test.assert. The user is asked to "trace through the pattern mentally." This is because, without a client API and without a discoverable bound port, there is no way to actually run a server-and-client round-trip purely in Seq. The capstone 05-echo shows an echo-handler body in a comment and never invokes it.

By chapter 26 the seqlings user has already done strand.spawn (ch 24), channels (ch 25), and is more than ready for a working loopback echo as the capstone — but the language won't let them. Once connect exists and listen returns the bound port, the rework writes itself:

: echo-server ( Socket Channel -- )
    swap dup net.tcp.listen ...   # spawn-side: send bound port over channel,
                                  # accept, read, echo, close
;

: test-echo ( -- )
    chan.make
    [ over echo-server ] strand.spawn drop
    chan.receive                   # get assigned port from server strand
    "127.0.0.1" swap net.tcp.connect
    [ "hi" over net.tcp.write drop
      dup net.tcp.read drop swap net.tcp.close drop
      "hi" test.assert-eq-str ]
    [ drop ] if
;

Notes

  • The tokio runtime backing listen/accept already does outbound TCP internally — connect is a small addition.
  • udp.bind already returns Socket Int Bool, so the precedent for the three-value listen return is in-tree.
  • Suggest filing the HTTP-server gap as a separate issue; this one is the immediate seqlings blocker.
## Summary `net.tcp.*` exposes only the server side: `listen`, `accept`, `read`, `write`, `close`. There is no way for a Seq program to **initiate** a TCP connection. UDP got this right (`net.udp.send-to`); TCP did not. Two gaps, both small: ### 1. Missing `net.tcp.connect` Proposed signature, symmetric with `net.udp.send-to` host/port shape: ``` net.tcp.connect ( a String Int -- a Socket Bool ) host port socket success ``` On success the returned `Socket` is interchangeable with one returned by `net.tcp.accept` — same `read` / `write` / `close` semantics. On failure: `(0, false)` matching the existing TCP convention. ### 2. `net.tcp.listen` should return the bound port Current: ``` net.tcp.listen ( a Int -- a Socket Bool ) port socket success ``` Compare UDP, which already does the right thing: ``` net.udp.bind ( a Int -- a Socket Int Bool ) port socket bound success ``` Requesting `port = 0` lets the OS pick an ephemeral port, but with the current TCP signature there is no way to discover what port was assigned. This makes loopback self-tests effectively impossible without hard-coding a port (and racing). Proposed: ``` net.tcp.listen ( a Int -- a Socket Int Bool ) port socket bound success ``` For non-zero `port`, `bound == port`. For `port == 0`, `bound` is the OS-assigned port. Matches the UDP convention exactly. ## Motivation Seqlings chapter 26 ("TCP") currently has five exercises that all amount to `# I AM NOT DONE` + `true test.assert`. The user is asked to "trace through the pattern mentally." This is because, without a client API and without a discoverable bound port, there is no way to actually run a server-and-client round-trip purely in Seq. The capstone `05-echo` shows an `echo-handler` body in a comment and never invokes it. By chapter 26 the seqlings user has already done `strand.spawn` (ch 24), channels (ch 25), and is more than ready for a working loopback echo as the capstone — but the language won't let them. Once `connect` exists and `listen` returns the bound port, the rework writes itself: ```seq : echo-server ( Socket Channel -- ) swap dup net.tcp.listen ... # spawn-side: send bound port over channel, # accept, read, echo, close ; : test-echo ( -- ) chan.make [ over echo-server ] strand.spawn drop chan.receive # get assigned port from server strand "127.0.0.1" swap net.tcp.connect [ "hi" over net.tcp.write drop dup net.tcp.read drop swap net.tcp.close drop "hi" test.assert-eq-str ] [ drop ] if ; ``` ## Notes - The tokio runtime backing `listen`/`accept` already does outbound TCP internally — `connect` is a small addition. - `udp.bind` already returns `Socket Int Bool`, so the precedent for the three-value listen return is in-tree. - Suggest filing the HTTP-server gap as a separate issue; this one is the immediate seqlings blocker.
Author
Owner

Scope narrowing — net.tcp.connect is done and shipped (thank you!). What remains in this issue is the second ask:

net.tcp.listen should return the bound port, symmetric with net.udp.bind's ( Int -- Socket Int Bool ). Currently listen returns ( Socket Bool ), which means port = 0 (let the OS pick) leaves the caller with no way to discover what port was assigned.

Practical impact: I just reworked seqlings chapter 26 to run real client/server programs, and had to hard-code ports 18261-18265 per exercise because loopback self-tests can't use ephemeral ports. Works fine in practice on single-user dev machines; would collide on shared CI. Not blocking, but the asymmetry with UDP is the only thing in this issue still open.

Suggested signature change:

net.tcp.listen  ( a Int -- a Socket Int Bool )
                  port    socket bound success

For non-zero port, bound == port. For port == 0, bound is the OS-assigned port. Same convention as UDP.

Scope narrowing — `net.tcp.connect` is done and shipped (thank you!). What remains in this issue is the second ask: **`net.tcp.listen` should return the bound port**, symmetric with `net.udp.bind`'s `( Int -- Socket Int Bool )`. Currently `listen` returns `( Socket Bool )`, which means `port = 0` (let the OS pick) leaves the caller with no way to discover what port was assigned. Practical impact: I just reworked seqlings chapter 26 to run real client/server programs, and had to hard-code ports 18261-18265 per exercise because loopback self-tests can't use ephemeral ports. Works fine in practice on single-user dev machines; would collide on shared CI. Not blocking, but the asymmetry with UDP is the only thing in this issue still open. Suggested signature change: ``` net.tcp.listen ( a Int -- a Socket Int Bool ) port socket bound success ``` For non-zero `port`, `bound == port`. For `port == 0`, `bound` is the OS-assigned port. Same convention as UDP.
Author
Owner
https://git.navicore.tech/navicore/patch-seq/pulls/485
Sign in to join this conversation.
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
navicore/patch-seq#475
No description provided.