ext_net: raw TCP sockets as tape-recorded nondeterministic inputs (#414) - #788
Merged
Conversation
Seven builtins — net_listen / net_port / net_accept / net_dial / net_recv / net_send / net_close, with poll-based timeouts — behind make net (EIGENSCRIPT_EXT_NET=1, in no default build; also compiled into make asan-http so the sanitizer gate covers the extension). Every nondeterministic outcome (accepted connections, received bytes, bytes-sent counts, dial results, kernel-assigned ephemeral ports) enters through TRACE_NONDET_TAKE/RECORD, so a recorded session replays byte-identically with no network present: $ EIGS_TRACE=t.tape eigenscript examples/net_echo.eigs > a $ EIGS_REPLAY=t.tape eigenscript examples/net_echo.eigs > b $ diff a b # identical; strace: 0 socket syscalls Replay-determinism rule (the #601 lesson): environment failures are recorded VALUES — null / -1 / empty buffer — never live-path-only raises, so a catch cannot desync the N-record stream; argument-shape errors raise deterministically before the tape boundary. net_send is a suppressed recorded write (the mkdir rule), documented in TRACE.md as the deliberate contrast to the #148 subprocess family. Sockets live in the process handle table (HANDLE_NET) and are closed by handle_table_drain at exit. Suite section [125] (probe-gated) runs 25 loopback echo checks plus the definition-of-done: record → replay byte-diff and a pinned 17-record tape accounting. TCP only for now — UDP stays open in #414. Gates: release 3280/3280, net 3308/3308, ASan 3284/3284, asan-http(+net) 3408/3408, leak tally 0; http/gfx/lsp/freestanding compile checks green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an opt-in ext_net extension that exposes raw TCP sockets while preserving EigenScript’s trace/replay determinism by routing all nondeterministic outcomes through the trace tape (TAKE/RECORD), and integrates it into builds, docs, CI, and the test suite.
Changes:
- Introduces
src/ext_net.c(+ private header) implementingnet_listen/net_port/net_accept/net_dial/net_recv/net_send/net_closewith poll-based timeouts and trace-tape wrapping. - Adds a dedicated test file plus a new probe-gated suite section that verifies record→replay byte identity and a pinned N-record count.
- Wires the extension into build targets, CI, lint’s builtin-name surface, and user docs/CHANGELOG.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_net.eigs | New single-process loopback tests for the net_* builtins, including timeout/EOF/value behavior expectations. |
| tests/run_all_tests.sh | Adds probe-gated section [125] running net tests and enforcing record→replay output equality + pinned tape accounting. |
| src/lint.c | Extends lint’s builtin-name sets to include EIGS_NET_BUILTINS. |
| src/ext_net.c | New extension implementation: TCP sockets + tape-wrapped nondeterminism + handle-table integration. |
| src/ext_net_internal.h | Private extension header defining EigsNetSock for handle-table storage/drain. |
| src/ext_names.h | Adds the EIGS_NET_BUILTINS macro list. |
| src/eigenscript.h | Adds EIGENSCRIPT_EXT_NET flag and HANDLE_NET handle type. |
| src/builtins.c | Registers net builtins when enabled; drains HANDLE_NET rows at teardown. |
| README.md | Documents make net build target. |
| Makefile | Adds net target; includes ext_net in full build and asan-http build. |
| examples/net_echo.eigs | New example program demonstrating record/replay with no network on replay. |
| docs/TRACE.md | Documents ext_net’s tape contract and replay suppression of net_send. |
| docs/BUILTINS.md | Documents the new net_* builtins and their semantics. |
| CHANGELOG.md | Adds an Unreleased entry describing ext_net and its trace/replay guarantees. |
| .github/workflows/ci.yml | Adds make net + full-suite run for the net variant. |
Suppressed comments (3)
src/ext_net.c:226
net_dialdoesn’t checkfcntl(F_GETFL)/fcntl(F_SETFL)results when switching the socket to nonblocking. If either fails,connect()may block indefinitely (breaking the timeout guarantee), or the fd may end up in a mode the later send/recv paths don’t handle (e.g., EAGAIN).
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
int rc = connect(fd, res->ai_addr, res->ai_addrlen);
src/ext_net.c:243
net_dialrestores the original fd flags withfcntl(fd, F_SETFL, flags)but doesn’t check for failure. If the restore fails, subsequentnet_send/net_recvmay behave unexpectedly (e.g., nonblocking EAGAIN) since those paths assume a blocking socket plus poll.
fcntl(fd, F_SETFL, flags);
src/ext_net.c:259
net_recvparsesmax_bytesandtimeout_mswithnet_num_at, which means a non-numerictimeout_mssilently becomes -1 (infinite block) and a non-numericmax_bytesbecomes 0 (then raises EK_VALUE). For consistency with the contract that argument-shape errors raise deterministically before the tape boundary, these should be validated as numbers explicitly.
int max = net_num_at(arg, 1, 0);
int timeout_ms = net_num_at(arg, 2, -1);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+175
to
+179
| if (arg && arg->type == VAL_LIST) { | ||
| if (!net_require_list(arg, 2, 2, "net_accept")) return make_null(); | ||
| timeout_ms = net_num_at(arg, 1, -1); | ||
| arg = arg->data.list.items[0]; | ||
| } |
Comment on lines
+209
to
+213
| int port = net_num_at(arg, 1, -1); | ||
| int timeout_ms = net_num_at(arg, 2, -1); | ||
| TRACE_NONDET_TAKE("net_dial"); | ||
| if (port < 0 || port > 65535) TRACE_NONDET_RECORD("net_dial", make_null()); | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #414 (TCP half; UDP and the liferaft real-socket demo stay open there).
What
src/ext_net.c— an opt-in extension in ext_http's shape (EIGENSCRIPT_EXT_NET=1, in no default build):net_listen/net_port/net_accept/net_dial/net_recv/net_send/net_close, with poll-based timeouts. Sockets areHANDLE_NETrows in the process handle table, drained at exit.The tape contract (the definition of done, not a follow-up)
TRACE_NONDET_TAKE/RECORD-wrapped. UnderEIGS_REPLAYthe TAKE short-circuits before any socket call: the replay run performs zero socket-family syscalls (strace-verified; live run: 6).null/-1/ empty buffer), never live-path-only raises — acatchcannot desync the N-record stream (the read_bytes_buf: over-cap read silently returns null (misleading downstream errors) + no opt-in for files >10MB #601 lesson). Argument-shape errors raise deterministically before the tape boundary.net_sendis a suppressed recorded write (themkdirrule): recorded count served under replay, nothing written. Documented in TRACE.md as the deliberate contrast to the proc_* builtins are invisible to trace/replay — EIGS_REPLAY re-runs live subprocess side effects #148 subprocess family.net_recvcaps at 8192 bytes/call so every N record fits the 64 KiB budget (theaudio_capture_readdiscipline).Acceptance
Suite section [125] (probe-gated, runs under
make netandmake asan-httpin CI): 25 loopback echo checks — a single-threaded program is both ends via the listen backlog — plus record→replay byte-diff and a pinned 17-record tape accounting check.Gates
make asan-http, so the extension stays sanitizer-covered in CIResidual (noted for the tape-first review)
net_closeracing a blockingnet_recvis the same exposure class as existing Channel/Thread handles. Single-threaded use is the v1 contract.getaddrinfoinnet_dialblocks without a timeout (DNS is resolved before the nonblocking-connect timeout applies).🤖 Generated with Claude Code