Skip to content

Rewrite the examples against the current TcpListener/TcpStream API - #15

Merged
hellerve merged 1 commit into
masterfrom
claude/fix-examples
Aug 18, 2026
Merged

Rewrite the examples against the current TcpListener/TcpStream API#15
hellerve merged 1 commit into
masterfrom
claude/fix-examples

Conversation

@carpentry-agent

Copy link
Copy Markdown
Contributor

Every file in examples/ was still written against the Socket module the
restructure into src/tcp_stream.carp / src/tcp_listener.carp removed, so
none of them compiled. On master:

file carp -b
examples/server.carp rc 1 — I couldn't find the symbol 'Socket.with-server'
examples/binary_server.carp rc 1 — Socket.with-server
examples/endless-server.carp rc 1 — Socket.with-server
examples/client.carp rc 1 — Socket.with-client
examples/binary_client.carp rc 1 — Socket.with-client

The 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
workflows permission, 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:

  • server / binary_serverTcpListener.bind, one accept, read, reply, close.
  • client / binary_clientTcpStream.connect, send, read, close.
  • endless-serverTcpListener.bind + TcpListener.while-accept.
  • text pair uses TcpStream.read / send, binary pair uses read-bytes / send-bytes.
  • Every Result is handled with match rather than an unsafe unwrap, so
    angler's unsafe-result-unwrap stays quiet. The old
    "client couldn't be started." placeholders are replaced by the actual error
    string the Result carries.
  • The (load "socket.carp") header becomes (load "../socket.carp"), matching
    test/. The old form only resolved when the compiler happened to be invoked
    from the repo root; the file-relative form builds from any working directory.

endless-server pulls its per-connection work into a small echo helper so the
while-accept call fits on one line. That is not cosmetic: carp-fmt has no
entry for library-defined body macros, so it treats while-accept as an
ordinary 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 on master).
  • Ran the pairs against each other on loopback, not just typechecked:
    • server + client: server printed hi from client, client printed hi, both exited 0.
    • binary_server + binary_client: server printed [1b 2b 3b], client printed [1b 2b 3b 0b], both exited 0.
    • endless-server + two sequential client runs: server logged both messages, each client got hi.
  • carp-fmt --check examples/*.carp — clean (all five would be reformatted on master).
  • angler examples/*.carp — clean.

Unrelated bug found while doing this

TcpStream.with-stream (and UnixStream.with-stream, same shape) cannot
typecheck in any context, which is why the clients use connect + match
instead:

I can't match the types `(Result h i)` and `()`.
  (do ... (TcpStream.close s)) : (Result h i)
  At line 255, column 58 in 'src/tcp_stream.carp'

The error arm expands to (Result.Error err) while the success arm always ends
in the cons-lasted (TcpStream.close name), which is (). Deciding what the
macro 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.

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.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 printed hi from client, client printed hi, both rc 0.
  • binary_server + binary_client — server printed [1b 2b 3b], client printed [1b 2b 3b 0b], both rc 0.
  • endless-server + three sequential client runs — all three clients got hi and rc 0; the server logged hi from client three 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 on master, so no regression either way).
  • carp-fmt --check examples/*.carp — clean. On master all 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-server printed a trailing "yay" that the rewrites drop. Deliberate and an improvement — flagging only so the diff's behaviour change isn't silent.
  • defn-do appears exactly once in the repo, in the new echo. It's a core macro so this is fine; the rationale for one-lining the while-accept body (carp-fmt has 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 (no workflows permission 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.

@hellerve
hellerve merged commit 822e440 into master Aug 18, 2026
2 checks passed
@hellerve
hellerve deleted the claude/fix-examples branch August 18, 2026 06:07
@carpentry-agent carpentry-agent Bot mentioned this pull request Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant