Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,14 @@ jobs:
name: Run full suite against zlib build (executes DEFLATE section [124])
run: cd tests && bash run_all_tests.sh

- if: needs.scope.outputs.code == 'true'
name: Build net variant
run: make net

- if: needs.scope.outputs.code == 'true'
name: Run full suite against net build (executes network section [125])
run: cd tests && bash run_all_tests.sh

- if: needs.scope.outputs.code == 'true'
name: Compile-check LSP
run: make lsp
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ All notable changes to EigenScript are documented here.

## [Unreleased]

### Added

- **`ext_net`: raw TCP sockets as tape-recorded nondeterministic inputs
(#414).** Seven builtins — `net_listen` / `net_port` / `net_accept` /
`net_dial` / `net_recv` / `net_send` / `net_close`, with timeouts —
behind `make net` (`EIGENSCRIPT_EXT_NET=1`, in no default build; also
compiled into `make asan-http` so the sanitizer gate covers it). The
differentiating contract: every nondeterministic outcome (accepted
connections, received bytes, bytes-sent counts, dial results,
kernel-assigned ephemeral ports) enters through the trace-tape seam,
so a recorded session replays **byte-identically with no network
present** — the replay run performs zero socket-family syscalls
(strace-verified; suite section [125] pins the byte-diff and the
tape's N-record count). Environment failures are recorded *values*
(`null` / `-1` / empty buffer), never live-path-only raises, so a
`catch` cannot desync replay; `net_send` is a suppressed recorded
write (the `mkdir` rule, documented in docs/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. `examples/net_echo.eigs` is a single-threaded both-ends echo
session over the loopback backlog. TCP only for now — UDP stays open
in #414.

### Changed

- **`-Werror=switch` was paid for in `CFLAGS` and then opted out of at almost
Expand Down
22 changes: 18 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ BINARY := $(SRC_DIR)/eigenscript
# --step tape-stepper, stdio+isatty, same footing).
CLI_ONLY := $(SRC_DIR)/main.c $(SRC_DIR)/repl.c $(SRC_DIR)/step.c $(SRC_DIR)/bundle.c

FULL_SOURCES := $(SOURCES) $(SRC_DIR)/ext_http.c $(SRC_DIR)/ext_db.c \
FULL_SOURCES := $(SOURCES) $(SRC_DIR)/ext_http.c $(SRC_DIR)/ext_db.c $(SRC_DIR)/ext_net.c \
$(SRC_DIR)/model_io.c $(SRC_DIR)/model_infer.c $(SRC_DIR)/model_train.c

PREFIX := $(HOME)/.local
Expand All @@ -39,7 +39,7 @@ PREFIX := $(HOME)/.local
LSP_SOURCES := $(SRC_DIR)/eigenlsp.c $(filter-out $(CLI_ONLY),$(SOURCES))
LSP_BINARY := $(SRC_DIR)/eigenlsp

.PHONY: all build full http gfx zlib lib amalgamation tsan test install install-gfx clean coverage coverage-clean fuzz fuzz-run lsp jit-smoke embed-smoke asan valgrind pgo freestanding-check freestanding-libc-diff asan-http print-%
.PHONY: all build full http net gfx zlib lib amalgamation tsan test install install-gfx clean coverage coverage-clean fuzz fuzz-run lsp jit-smoke embed-smoke asan valgrind pgo freestanding-check freestanding-libc-diff asan-http print-%

# Introspection helper: `make print-SOURCES` echoes a variable's value.
# tests/test_leak_guard.sh derives its ASan build source list from the
Expand All @@ -62,6 +62,7 @@ build:
full:
$(CC) $(CFLAGS) -o $(BINARY) $(FULL_SOURCES) \
-I/usr/include/postgresql \
-DEIGENSCRIPT_EXT_NET=1 \
-DEIGENSCRIPT_VERSION='"$(VERSION)"' \
$(LDFLAGS) -lpq
@echo "EigenScript $(VERSION) (full) built. Binary: $$(du -sh $(BINARY) | cut -f1)"
Expand Down Expand Up @@ -93,6 +94,18 @@ zlib:
$(LDFLAGS) -lz
@echo "EigenScript $(VERSION) (zlib) built. Binary: $$(du -sh $(BINARY) | cut -f1)"

# Raw TCP sockets on the trace tape (#414). Same opt-in pattern as gfx:
# in no default build, no extra library needed (plain POSIX sockets).
net:
$(CC) $(CFLAGS) -o $(BINARY) $(SOURCES) $(SRC_DIR)/ext_net.c \
-DEIGENSCRIPT_EXT_HTTP=0 \
-DEIGENSCRIPT_EXT_MODEL=0 \
-DEIGENSCRIPT_EXT_DB=0 \
-DEIGENSCRIPT_EXT_NET=1 \
-DEIGENSCRIPT_VERSION='"$(VERSION)"' \
$(LDFLAGS)
@echo "EigenScript $(VERSION) (net) built. Binary: $$(du -sh $(BINARY) | cut -f1)"

gfx:
$(CC) $(CFLAGS) -o $(BINARY) $(SOURCES) $(SRC_DIR)/ext_gfx.c \
-DEIGENSCRIPT_EXT_HTTP=0 \
Expand Down Expand Up @@ -204,14 +217,15 @@ asan:
# make asan-http && cd tests && ASAN_OPTIONS=detect_leaks=1 bash run_all_tests.sh
asan-http:
$(CC) -fsanitize=address,undefined -Werror=switch -g -O1 -o $(BINARY) $(SOURCES) \
$(SRC_DIR)/ext_http.c \
$(SRC_DIR)/ext_http.c $(SRC_DIR)/ext_net.c \
$(SRC_DIR)/model_io.c $(SRC_DIR)/model_infer.c $(SRC_DIR)/model_train.c \
-DEIGENSCRIPT_EXT_HTTP=1 \
-DEIGENSCRIPT_EXT_MODEL=1 \
-DEIGENSCRIPT_EXT_DB=0 \
-DEIGENSCRIPT_EXT_NET=1 \
-DEIGENSCRIPT_VERSION='"$(VERSION)"' \
-lm -lpthread
@echo "EigenScript $(VERSION) (asan+ubsan, http+model) built. Binary: $(BINARY)"
@echo "EigenScript $(VERSION) (asan+ubsan, http+model+net) built. Binary: $(BINARY)"

# ThreadSanitizer build for the concurrency race gate (tests/test_tsan.sh).
# Complements ASan (which is not run with the thread checker). Run the tests
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ get made and how contributors can earn commit access over time.
make # build
make test # build and run the full suite (2,500+ checks)
make gfx # build with SDL2 graphics (UI toolkit, games)
make net # build with raw TCP sockets (record/replay-able)
make install # install to ~/.local/bin
make clean # remove build artifacts
```
Expand Down
29 changes: 29 additions & 0 deletions docs/BUILTINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,35 @@ automatically at exit.
|------|-----------|-------------|
| `build_corpus` | `build_corpus of [files, top_n, stream_path, vocab_path]` | Three-pass C-backed corpus builder: tokenise `files`, emit top-`n` vocabulary and stream-format token IDs |

## Optional: Network Extension (TCP sockets)

Requires a `make net` build (`EIGENSCRIPT_EXT_NET=1`; in no default
build). Raw TCP sockets whose every nondeterministic outcome — accepted
connections, received bytes, bytes-sent counts, dial results,
kernel-assigned ports — rides the trace tape: a session recorded under
`EIGS_TRACE` replays byte-identically under `EIGS_REPLAY` with **no
network present** (the replay run performs zero socket syscalls). See
[TRACE.md](TRACE.md).

| Builtin | Form | Returns |
|---------|------|---------|
| `net_listen` | `net_listen of port` | listener handle, or `null` (bind failed). Port `0` = kernel-assigned ephemeral port |
| `net_port` | `net_port of listener` | the locally bound port (the kernel's pick for port 0), or `null` |
| `net_accept` | `net_accept of listener` / `net_accept of [listener, timeout_ms]` | connection handle, or `null` on timeout |
| `net_dial` | `net_dial of [host, port]` / `net_dial of [host, port, timeout_ms]` | connection handle, or `null` (refused / unresolvable / timeout) |
| `net_recv` | `net_recv of [conn, max_bytes]` / `net_recv of [conn, max_bytes, timeout_ms]` | buffer of byte values (empty buffer = connection over), or `null` on timeout. `max_bytes` is clamped to 8192 per call — loop to drain; decode text with `str_from_bytes` |
| `net_send` | `net_send of [conn, data]` — `data` is a string, buffer, or byte list | bytes sent, or `-1` (peer gone / bad handle) |
| `net_close` | `net_close of handle` | `null`; idempotent |

Environment failures are *values* (`null` / `-1` / empty buffer), never
raises, so every outcome lands on the tape and a `catch` cannot desync
replay; argument-shape mistakes (wrong type or arity) raise
deterministically. A single-threaded program can be both ends of a
connection: on loopback, `net_dial` completes against the listen
backlog before `net_accept` runs (see `examples/net_echo.eigs`).
Sockets left open at exit are closed by the runtime's handle-table
drain. UDP is not yet exposed (#414 tracks it).

## Optional: HTTP Extension

Requires full build. Provides an embedded HTTP server.
Expand Down
21 changes: 21 additions & 0 deletions docs/TRACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,27 @@ perspective lands on the tape as an `N` record:
the live argv; #471)
- **HTTP extension:** `http_post` (success and all error paths),
`http_request_body`, `http_session_id`, `http_request_headers`
- **Network extension (#414):** `net_listen`, `net_port`, `net_accept`,
`net_dial`, `net_recv`, `net_send`. Every environment outcome is a
recorded *value* — `null` for failure/timeout, a number or buffer for
success — never a live-path-only raise, so a `catch` cannot desync
the record stream (the `read_bytes_buf` lesson). The whole family is
TAKE/RECORD-wrapped: under `EIGS_REPLAY` the tape is taken **before
any socket call** — the replay run creates, binds, connects, reads,
and writes **nothing** (verified by strace: zero socket-family
syscalls), which is what "replay last night's flaky network failure
with the network gone" means. `net_recv` caps at 8192 bytes per call
so every `N` record fits the 64 KiB budget, the same discipline as
`audio_capture_read`. `net_send` is a *recorded write*, like `mkdir`:
its observable effect on the program is its result (bytes sent), and
the peer's future responses are themselves on the tape — so under
replay the send is **suppressed** (recorded count served, nothing
written) and the replayed world stays consistent. That is the
deliberate contrast with the #148 subprocess family below: a
`proc_write` feeds a live child whose behavior the tape does not pin,
so suppressing it would be meaningless. (`net_close` is deterministic
and untraced — under replay no socket exists and it is a natural
no-op, the `audio_capture_close` shape.)
- **Audio capture (gfx extension, #579):** `audio_capture_open`,
`audio_capture_read`. Captured audio is a device input, so the whole
capture chain is TAKE/RECORD-wrapped: under `EIGS_REPLAY` the tape is
Expand Down
46 changes: 46 additions & 0 deletions examples/net_echo.eigs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# net_echo — raw TCP sockets on the trace tape (#414).
# Requires a `make net` build (the net_* builtins are in no default build).
#
# One process plays both ends of a TCP connection over loopback: the
# dial completes against the listen backlog, so no threads are needed.
# Every nondeterministic outcome — the kernel's ephemeral port, the
# accepted connection, the received bytes, the bytes-sent counts —
# enters through the trace tape, which makes this session replayable
# with no network at all:
#
# $ EIGS_TRACE=echo.tape eigenscript net_echo.eigs > first.out
# $ EIGS_REPLAY=echo.tape eigenscript net_echo.eigs > second.out
# $ diff first.out second.out # identical
#
# The replay run performs zero socket syscalls — the tape pins what the
# network said, so the network is no longer required to say it.

# Ask the kernel for an ephemeral port (0), then read back its pick.
server is net_listen of 0
port is net_port of server
print of "listening"

# Dial ourselves. On loopback the connect lands in the listen backlog
# immediately, before we ever call accept.
client is net_dial of ["127.0.0.1", port]
conn is net_accept of [server, 2000]

# Client -> server. net_recv returns a buffer of byte values (binary-
# safe); str_from_bytes decodes text. The 2000 is a timeout in ms —
# a timeout returns null, a closed peer returns an empty buffer.
sent is net_send of [client, "ping over the tape"]
print of sent
data is net_recv of [conn, 256, 2000]
msg is str_from_bytes of (data)
print of msg

# Server echoes it back.
echoed is net_send of [conn, msg]
print of echoed
back is net_recv of [client, 256, 2000]
print of str_from_bytes of (back)

net_close of client
net_close of conn
net_close of server
print of "echo complete"
23 changes: 23 additions & 0 deletions src/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
#include "ext_db_internal.h"
#endif

#if EIGENSCRIPT_EXT_NET
#include "ext_net_internal.h"
#include <unistd.h> /* close() in handle_table_drain's HANDLE_NET pass */
#endif

#if EIGENSCRIPT_EXT_MODEL
#include "model_internal.h"
#endif
Expand Down Expand Up @@ -5834,6 +5839,20 @@ void handle_table_drain(EigsState *st) {
free(ch);
st->handle_table[i].ptr = NULL;
}
#if EIGENSCRIPT_EXT_NET
/* #414 sockets: an outstanding listener/connection at exit is an OS fd
* plus one malloc'd row — close and free. No thread can still be using
* it: every worker was joined above. */
for (int i = 1; i < HANDLE_TABLE_SIZE; i++) {
if (st->handle_table[i].type != HANDLE_NET) continue;
EigsNetSock *ns = (EigsNetSock*)st->handle_table[i].ptr;
if (!ns) continue;
close(ns->fd);
free(ns);
st->handle_table[i].ptr = NULL;
}
#endif

/* #408 tasks: cooperative, single-thread, no OS resource — just held
* refs. An outstanding task at exit (never joined, or the program ended
* with it still live) is reclaimed here. Its held entry_fn/args/result
Expand Down Expand Up @@ -7598,6 +7617,10 @@ void register_builtins(Env *env) {
register_db_builtins(env);
#endif

#if EIGENSCRIPT_EXT_NET
register_net_builtins(env);
#endif

#if EIGENSCRIPT_EXT_MODEL
register_model_builtins(env);
#endif
Expand Down
8 changes: 7 additions & 1 deletion src/eigenscript.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
#ifndef EIGENSCRIPT_EXT_GFX
#define EIGENSCRIPT_EXT_GFX 0
#endif
/* Raw TCP sockets on the trace tape (#414). Default OFF like GFX: in no
* default build — `make net` opts in (src/ext_net.c). */
#ifndef EIGENSCRIPT_EXT_NET
#define EIGENSCRIPT_EXT_NET 0
#endif
/* DEFLATE codecs (inflate/deflate via the system zlib, -lz). Default OFF
* like GFX: the minimal build stays zero-dependency — the four builtins
* stay registered but raise "compiled without zlib support" until
Expand Down Expand Up @@ -473,7 +478,8 @@ typedef enum {
HANDLE_STORE,
HANDLE_THREAD,
HANDLE_CHANNEL,
HANDLE_TASK /* #408 cooperative task — id-keyed, drained at teardown */
HANDLE_TASK, /* #408 cooperative task — id-keyed, drained at teardown */
HANDLE_NET /* #414 ext_net socket (listener or connection) */
} HandleType;

typedef struct {
Expand Down
10 changes: 10 additions & 0 deletions src/ext_names.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@
X(http_serve, builtin_http_serve) \
X(http_cors, builtin_http_cors)

/* ---- ext_net.c: raw TCP sockets on the tape (EIGENSCRIPT_EXT_NET) ---- */
#define EIGS_NET_BUILTINS(X) \
X(net_listen, builtin_net_listen) \
X(net_port, builtin_net_port) \
X(net_accept, builtin_net_accept) \
X(net_dial, builtin_net_dial) \
X(net_recv, builtin_net_recv) \
X(net_send, builtin_net_send) \
X(net_close, builtin_net_close)

/* ---- ext_db.c: SQLite bridge (EIGENSCRIPT_EXT_DB) ---- */
#define EIGS_DB_BUILTINS(X) \
X(db_connect, builtin_db_connect) \
Expand Down
Loading
Loading