Rewrite the examples against the current TcpListener/TcpStream API - #15
Conversation
All five files in examples/ still called the Socket module that the restructure into src/tcp_*.carp removed, so none of them compiled: carp -b examples/server.carp died with "I couldn't find the symbol 'Socket.with-server'", and the other four failed the same way on Socket.with-server / Socket.with-client. They now follow the shape the README already documents: TcpListener.bind plus accept (or while-accept for the endless server) on the server side, TcpStream.connect on the client side, and match on every Result instead of unwrapping. The bare "client couldn't be started" messages are replaced by the actual error string the Result carries. endless-server pulls its per-connection work into a helper so the while-accept call stays on one line; carp-fmt has no entry for library-defined body macros and hanging-indents a multi-form call out to column 36. Every example builds (carp -b, rc 0) and both pairs were run against each other on loopback, plus two clients against the endless server.
There was a problem hiding this comment.
Build & Tests
Checked out claude/fix-examples (b3da7be, one commit on top of master) and built everything locally with CARP_DIR=~/carp-lang.
Before/after, measured rather than taken from the description:
| file | carp -b on master |
on this branch |
|---|---|---|
examples/server.carp |
rc 1 — I couldn't find the symbol 'Socket.with-server' |
rc 0 |
examples/binary_server.carp |
rc 1 — same | rc 0 |
examples/endless-server.carp |
rc 1 — same | rc 0 |
examples/client.carp |
rc 1 — 'Socket.with-client' |
rc 0 |
examples/binary_client.carp |
rc 1 — same | rc 0 |
So the "none of them compiled" premise holds, with the exact errors claimed.
They also run. carp -b overwrites the same ~/.carp/out/Untitled every time, so I built each one and copied the binary aside before building the next (five distinct binaries, distinct sizes), then ran the pairs on loopback:
server+client— server printedhi from client, client printedhi, both rc 0.binary_server+binary_client— server printed[1b 2b 3b], client printed[1b 2b 3b 0b], both rc 0.endless-server+ three sequentialclientruns — all three clients gothiand rc 0; the server loggedhi from clientthree times.
One harness note in case it saves someone the same detour: endless-server never exits, so its stdout is block-buffered when redirected to a file and a kill loses the log. Under stdbuf -o0 the messages are all there. That was my capture, not the example.
CI: test (ubuntu-latest) and test (macos-latest) both pass.
Lint/format, run locally because CI excludes examples/ from both steps (-not -path './examples/*'):
angler examples/*.carp— clean (also clean onmaster, so no regression either way).carp-fmt --check examples/*.carp— clean. Onmasterall five report would be reformatted, so that claim isn't vacuous.
Findings
I tried to break this and could not. The things I checked beyond the suite:
The while-accept close is not a leak. endless-server's echo helper deliberately doesn't close the client, and that's correct — TcpListener.while-accept cons-lasts (TcpStream.close name) into the body itself (src/tcp_listener.carp:71). Closing in echo too would be a double close. The single-accept examples do close, and both close the listener afterwards.
Hostile input. I hit endless-server with three clients that connect and drop immediately without sending anything. It stayed alive, and a normal client served correctly afterwards. Worth knowing what the reader sees: TcpStream.read on a dropped peer returns Result.Success "", not an Error, so the example prints a blank line per dead connection rather than reporting the disconnect. That's library behaviour the example is faithfully reflecting, not something this PR introduced — I mention it only because "empty line" is what someone copying this will observe.
The load-path change is load-bearing, not cosmetic. Verified both directions from a cwd outside the repo: the old (load "socket.carp") resolves relative to the working directory and dies with I can't find a file named: 'socket.carp'; the new (load "../socket.carp") builds rc 0 from anywhere. It also matches what every file in test/ already does.
The disclosed with-stream bug is real. I reproduced it standalone rather than taking it on trust:
I can't match the types `(Result h i)` and `()`.
(do (let [_ (TcpStream.send (ref s) "x")] ()) (TcpStream.close s)) : (Result h i)
At line 255, column 58 in 'src/tcp_stream.carp'
TcpStream.with-stream cannot typecheck in any context — the error arm yields (Result.Error err) and the success arm ends in the cons-lasted close, which is (). UnixStream.with-stream has the identical shape. Not using it in the examples was the right call, and leaving the fix to you rather than guessing at the intended return type is the right call too. Worth an issue.
No CHANGELOG in this repo, so nothing missed there.
Two small notes, neither blocking:
- The old
server/endless-serverprinted a trailing"yay"that the rewrites drop. Deliberate and an improvement — flagging only so the diff's behaviour change isn't silent. defn-doappears exactly once in the repo, in the newecho. It's a core macro so this is fine; the rationale for one-lining thewhile-acceptbody (carp-fmthas no entry for library-defined body macros and hanging-indents it to column 36) checks out.examples/still isn't compiled by CI, so this can rot again the same way. The PR says why it didn't add the step (noworkflowspermission on the app) — that leaves a small human-authored change worth doing, since this rot was silent for an entire restructure.
Verdict: merge
Every claim in the description is independently verified, all five examples go from broken to building and running correctly, both CI runners are green, and the one bug found along the way is disclosed with a correct diagnosis instead of a guessed fix.
Every file in
examples/was still written against theSocketmodule therestructure into
src/tcp_stream.carp/src/tcp_listener.carpremoved, sonone of them compiled. On
master:carp -bexamples/server.carpI couldn't find the symbol 'Socket.with-server'examples/binary_server.carpSocket.with-serverexamples/endless-server.carpSocket.with-serverexamples/client.carpSocket.with-clientexamples/binary_client.carpSocket.with-clientThe README was updated when the library was restructured;
examples/was not.It rotted silently because CI never touches it — there is no examples step in
the workflow, and the lint and format-check steps both
-not -path './examples/*'.(I have not added a CI step here: the app this branch was pushed with has no
workflowspermission, so that has to be a separate, human-authored change.)What changed
Each example keeps its original intent and follows the shape the README already
documents:
TcpListener.bind, oneaccept, read, reply, close.TcpStream.connect, send, read, close.TcpListener.bind+TcpListener.while-accept.TcpStream.read/send, binary pair usesread-bytes/send-bytes.Resultis handled withmatchrather than an unsafe unwrap, soangler'sunsafe-result-unwrapstays quiet. The old"client couldn't be started."placeholders are replaced by the actual errorstring the
Resultcarries.(load "socket.carp")header becomes(load "../socket.carp"), matchingtest/. The old form only resolved when the compiler happened to be invokedfrom the repo root; the file-relative form builds from any working directory.
endless-serverpulls its per-connection work into a smallechohelper so thewhile-acceptcall fits on one line. That is not cosmetic:carp-fmthas noentry for library-defined body macros, so it treats
while-acceptas anordinary call and hanging-indents a multi-form body out to column 36. One-lining
the call sidesteps that without touching the formatter.
Verification
carp -b examples/<file>— rc 0 for all five (rc 1 for all five onmaster).server+client: server printedhi from client, client printedhi, both exited 0.binary_server+binary_client: server printed[1b 2b 3b], client printed[1b 2b 3b 0b], both exited 0.endless-server+ two sequentialclientruns: server logged both messages, each client gothi.carp-fmt --check examples/*.carp— clean (all five would be reformatted onmaster).angler examples/*.carp— clean.Unrelated bug found while doing this
TcpStream.with-stream(andUnixStream.with-stream, same shape) cannottypecheck in any context, which is why the clients use
connect+matchinstead:
The error arm expands to
(Result.Error err)while the success arm always endsin the
cons-lasted(TcpStream.close name), which is(). Deciding what themacro should return is your call, so I have left it alone rather than guess —
happy to file it as an issue or fix it in a follow-up.
Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.