Implement Phase 7: Gerbil porting features (spawn, atoms, rwlocks, TCP, process ports, with-lock)

ober

0bcc3398c9501a1722caf6a30381f6c6fc8456ae

diff --git a/Makefile b/Makefile
index 6fc0ca5..14047ab 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,7 @@ CHEZ_EXT_LIBDIRS = $(CHEZ_EXT_DIR)/chez-https/src:$(CHEZ_EXT_DIR)/chez-ssl/src:$
 # Shared object paths for FFI-based chez-* libraries
 CHEZ_EXT_LDPATH = $(CHEZ_EXT_DIR)/chez-ssl:$(CHEZ_EXT_DIR)/chez-zlib:$(CHEZ_EXT_DIR)/chez-pcre2:$(CHEZ_EXT_DIR)/chez-leveldb:$(CHEZ_EXT_DIR)/chez-epoll:$(CHEZ_EXT_DIR)/chez-inotify:$(CHEZ_EXT_DIR)/chez-crypto:$(CHEZ_EXT_DIR)/chez-sqlite:$(CHEZ_EXT_DIR)/chez-postgresql
 
-.PHONY: test test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-functional clean
+.PHONY: test test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-functional clean
 
 test: test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded
 
@@ -224,6 +224,10 @@ test-phase6:
 	@echo "--- Phase 6: Making Real Programs Easier to Build ---"
 	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-phase6.ss
 
+test-phase7:
+	@echo "--- Phase 7: Gerbil Porting Features ---"
+	@$(SCHEME) --libdirs $(LIBDIRS) --program tests/test-phase7.ss
+
 test-functional:
 	@echo "--- Functional Tests (real I/O, fork, Landlock, signals) ---"
 	@gcc -shared -fPIC -O2 -o support/libjerboa-landlock.so support/landlock-shim.c 2>/dev/null || true
diff --git a/docs/implement.md b/docs/implement.md
index fe1727c..e15b467 100644
--- a/docs/implement.md
+++ b/docs/implement.md
@@ -3405,3 +3405,200 @@ Track 28 (Error Diagnostics) ← uses inspect/object, independent
 Build order: 20 → (21, 22, 23, 27, 28 in parallel) → (24, 25, 26) → 29
 
 Phase 6 turns Jerboa from a language that *can* build systems programs (with enough C glue and platform hacks) into a language that makes systems programming *natural*. The difference: 1,977 lines of workarounds become 260 lines of clean imports.
+
+---
+
+# Phase 7: Gerbil Application Porting — The Missing Pieces
+
+## Motivation
+
+Jerboa already has extensive Gerbil API compatibility: hash tables (`hash-put!`/`hash-get`), `defstruct`, `defclass`, `defmethod`, `match`, `try`/`catch`/`finally`, channels, threads (Gambit API), and most of `:std/sugar`. However, porting real Gerbil applications (like gerbil-emacs, ~88K lines) reveals specific gaps that cause friction across nearly every source file.
+
+### What Already Works
+
+| Feature | Module | Status |
+|---------|--------|--------|
+| Hash tables (Gerbil API) | `(jerboa runtime)` | Complete — `hash-put!`, `hash-get`, `hash-ref`, `hash->list`, etc. |
+| `defstruct` / `defclass` | `(jerboa core)` | Complete — with inheritance support |
+| `defmethod` | `(jerboa core)` | Complete — runtime method dispatch |
+| `match` | `(jerboa core)` | Complete — pattern matching |
+| `try`/`catch`/`finally` | `(std sugar)` | Complete |
+| `while`/`until` | `(std sugar)` | Complete |
+| `unwind-protect` | `(std sugar)` | Complete |
+| Channels | `(std misc channel)` | Complete — O(1) ring buffer with `channel-select` |
+| Threads (Gambit API) | `(std misc thread)` | Complete — `make-thread`, `thread-start!`, `thread-join!` |
+| Mutexes/Condvars | `(std misc thread)` | Complete — Gambit-compatible API |
+| Thread mailboxes | `(std misc thread)` | Complete — `thread-send`, `thread-receive` |
+| JSON | `(std text json)` | Complete |
+| Format/printf | `(std format)` | Complete |
+| SRFI-13 | `(std srfi srfi-13)` | Complete |
+| SRFI-19 | `(std srfi srfi-19)` | Complete |
+| Process execution | `(std misc process)` | Partial — `run-process` returns string, no port access |
+| POSIX FFI | `(std os posix)` | Complete — pipe, fork, open, stat, etc. |
+| FD management | `(std os fd)` | Complete — `spawn-process`, `fd-pipe`, `with-fds` |
+
+### What's Missing (This Phase)
+
+| Track | Feature | Impact | Est. Lines |
+|-------|---------|--------|-----------|
+| 30 | `spawn` / `spawn/name` / `spawn/group` | Every background task | ~30 |
+| 31 | Atoms (`atom`, `atom-deref`, `atom-reset!`, `atom-swap!`) | Thread-safe state | ~40 |
+| 32 | Read-Write Locks (`make-rwlock`, `with-read-lock`, `with-write-lock`) | Shared state | ~60 |
+| 33 | TCP Server (`tcp-listen`, `tcp-accept`, `tcp-connect`) | Networking/IPC | ~200 |
+| 34 | Process Ports (`open-input-process`, `open-output-process`, `process-port-pid`) | Shell/REPL/linter | ~120 |
+| 35 | `with-lock` macro and `unwind-protect` enhancements | Cleanup patterns | ~20 |
+| **Total** | | | **~470** |
+
+---
+
+## Track 30: `spawn` / `spawn/name` / `spawn/group`
+
+**Module**: `(std misc thread)` — extend existing
+
+**What**: Gerbil's `spawn` is the primary way to create background threads. It's used ~50 times in gerbil-emacs alone.
+
+```scheme
+(spawn thunk)                    ;; → thread (started immediately)
+(spawn/name "worker" thunk)      ;; → thread with name
+(spawn/group "pool" thunk)       ;; → thread in named group
+```
+
+**Implementation**: Thin wrappers around existing `make-thread` + `thread-start!`.
+
+```scheme
+(define (spawn thunk)
+  (thread-start! (make-thread thunk)))
+
+(define (spawn/name name thunk)
+  (thread-start! (make-thread thunk name)))
+
+(define (spawn/group group thunk)
+  (thread-start! (make-thread thunk group)))
+```
+
+---
+
+## Track 31: Atoms — Thread-Safe Mutable References
+
+**Module**: `(std misc atom)` — new
+
+**What**: Gerbil's `atom` is a mutable cell with optional mutex protection, used for background thread state (file indices, caches). Used ~20 times in gerbil-emacs.
+
+```scheme
+(define counter (atom 0))
+(atom-deref counter)            ;; → 0
+(atom-reset! counter 42)        ;; set to 42
+(atom-swap! counter add1)       ;; atomically apply function
+(atom-update! counter + 10)     ;; atomically apply with extra args
+```
+
+**Implementation**: Record type + mutex for thread safety.
+
+---
+
+## Track 32: Read-Write Locks
+
+**Module**: `(std misc rwlock)` — new
+
+**What**: Gerbil's `:std/misc/rwlock` provides concurrent-read / exclusive-write locking. Used for shared data structures accessed from multiple threads.
+
+```scheme
+(define lock (make-rwlock))
+(with-read-lock lock (lambda () (read-shared-data)))
+(with-write-lock lock (lambda () (update-shared-data!)))
+```
+
+**Implementation**: Classic readers-writer lock via Chez mutex + condition variables.
+
+---
+
+## Track 33: TCP Server — Socket Networking
+
+**Module**: `(std net tcp)` — new
+
+**What**: Gerbil applications use `open-tcp-server` for IPC (emacsclient-like remote control), HTTP servers, and service networking. This is a hard requirement for any networked application.
+
+```scheme
+;; Server
+(define server (tcp-listen "127.0.0.1" 8080))
+(let-values ([(in out) (tcp-accept server)])
+  (display "hello\n" out)
+  (flush-output-port out)
+  (close-port in)
+  (close-port out))
+(tcp-close server)
+
+;; Client
+(let-values ([(in out) (tcp-connect "127.0.0.1" 8080)])
+  (display (read-line in))
+  (close-port in)
+  (close-port out))
+```
+
+**Implementation**: POSIX socket FFI (`socket`, `bind`, `listen`, `accept`, `connect`) wrapped into Scheme ports via `make-custom-binary-input/output-port` or Chez's transcoded ports.
+
+---
+
+## Track 34: Process Ports — Subprocess I/O as Ports
+
+**Module**: `(std misc process)` — extend existing
+
+**What**: Gerbil's `open-process` returns a bidirectional port connected to a subprocess. Used for interactive shells, REPLs, and linters that need to send input and read output incrementally (not batch).
+
+```scheme
+;; Open a subprocess with port-based I/O
+(let ([proc (open-input-process '("ls" "-la"))])
+  (let loop ()
+    (let ([line (read-line proc)])
+      (unless (eof-object? line)
+        (displayln line)
+        (loop))))
+  (close-input-port proc))
+
+;; Bidirectional
+(let ([proc (open-process '("python3" "-i"))])
+  (display "print(1+2)\n" proc)
+  (flush-output-port proc)
+  (displayln (read-line proc))
+  (close-port proc))
+```
+
+**Implementation**: Uses POSIX `pipe` + `fork` + `execvp` to create subprocess, then wraps the pipe FDs as Chez Scheme ports. Builds on existing `(std os posix)` and `(std io raw)`.
+
+---
+
+## Track 35: `with-lock` Macro
+
+**Module**: `(std sugar)` — extend existing
+
+**What**: Common Gerbil pattern for mutex-protected critical sections. Cleaner than manual `mutex-lock!`/`unwind-protect`/`mutex-unlock!`.
+
+```scheme
+(with-lock my-mutex
+  (modify-shared-state!))
+```
+
+**Implementation**: Syntax-rules macro expanding to `dynamic-wind` or `unwind-protect`.
+
+---
+
+## Dependency Order
+
+```
+Track 30 (spawn) ← extends (std misc thread), no deps
+Track 31 (atoms) ← uses Chez mutexes, no deps
+Track 32 (rwlock) ← uses Chez mutexes + conditions, no deps
+Track 33 (TCP) ← uses (std os posix) for socket FFI
+Track 34 (process ports) ← uses (std os posix) + (std io raw)
+Track 35 (with-lock) ← extends (std sugar), no deps
+```
+
+Build order: (30, 31, 32, 35 in parallel) → (33, 34)
+
+## Porting Effort After Phase 7
+
+With these ~470 lines implemented, porting gerbil-emacs becomes mechanical translation:
+- **100% of import lines** map to jerboa modules
+- **100% of concurrency patterns** have direct equivalents
+- **100% of data structures** have compatible APIs
+- Remaining work is Gambit→Chez FFI translation (`begin-ffi` → `foreign-procedure`) which is module-specific, not pervasive
diff --git a/lib/std/misc/atom.sls b/lib/std/misc/atom.sls
new file mode 100644
index 0000000..b25ee23
--- /dev/null
+++ b/lib/std/misc/atom.sls
@@ -0,0 +1,52 @@
+#!chezscheme
+;;; :std/misc/atom -- Thread-safe mutable reference cells
+;;;
+;;; Gerbil's atom API: a mutable cell with mutex-protected updates.
+;;; Used for background thread state (caches, indices, flags).
+;;;
+;;; (define counter (atom 0))
+;;; (atom-deref counter)          ;; → 0
+;;; (atom-reset! counter 42)      ;; set to 42
+;;; (atom-swap! counter add1)     ;; atomically apply function → 1
+;;; (atom-update! counter + 10)   ;; atomically apply with args → 11
+
+(library (std misc atom)
+  (export atom atom? atom-deref atom-reset! atom-swap! atom-update!)
+
+  (import (except (chezscheme) atom?))
+
+  (define-record-type atom-rec
+    (fields
+      (mutable val)
+      (immutable mtx))
+    (sealed #t))
+
+  (define (atom initial-value)
+    (make-atom-rec initial-value (make-mutex)))
+
+  (define (atom? x) (atom-rec? x))
+
+  (define (atom-deref a)
+    (with-mutex (atom-rec-mtx a)
+      (atom-rec-val a)))
+
+  (define (atom-reset! a new-val)
+    (with-mutex (atom-rec-mtx a)
+      (atom-rec-val-set! a new-val)
+      new-val))
+
+  (define (atom-swap! a fn)
+    ;; Atomically apply fn to current value, store and return result.
+    (with-mutex (atom-rec-mtx a)
+      (let ([new-val (fn (atom-rec-val a))])
+        (atom-rec-val-set! a new-val)
+        new-val)))
+
+  (define (atom-update! a fn . args)
+    ;; Atomically apply (fn current-val args ...), store and return result.
+    (with-mutex (atom-rec-mtx a)
+      (let ([new-val (apply fn (atom-rec-val a) args)])
+        (atom-rec-val-set! a new-val)
+        new-val)))
+
+  ) ;; end library
diff --git a/lib/std/misc/process.sls b/lib/std/misc/process.sls
index 86c0603..5ae87f2 100644
--- a/lib/std/misc/process.sls
+++ b/lib/std/misc/process.sls
@@ -4,7 +4,18 @@
 (library (std misc process)
   (export
     run-process
-    run-process/batch)
+    run-process/batch
+
+    ;; Process ports (Gambit-compatible subprocess I/O)
+    open-input-process
+    open-output-process
+    open-process
+    process-port-pid
+    process-port?
+    process-port-status
+    process-port-rec-stdin-port
+    process-port-rec-stdout-port
+    process-port-rec-stderr-port)
 
   (import (chezscheme))
 
@@ -109,4 +120,52 @@
             (apply string-append (reverse chunks)))
           (lp (cons buf chunks))))))
 
+  ;; ========== Process Ports (Gambit-compatible) ==========
+  ;; These wrap Chez's open-process-ports to provide the simpler
+  ;; Gambit API used by Gerbil applications.
+
+  (define-record-type process-port-rec
+    (fields
+      (immutable stdin-port)    ;; port to write to process stdin (or #f)
+      (immutable stdout-port)   ;; port to read process stdout (or #f)
+      (immutable stderr-port)   ;; port to read process stderr (or #f)
+      (immutable pid)           ;; process id
+      (mutable status))         ;; exit status (set when reaped)
+    (sealed #t))
+
+  (define (process-port? x) (process-port-rec? x))
+  (define (process-port-pid pp) (process-port-rec-pid pp))
+  (define (process-port-status pp) (process-port-rec-status pp))
+
+  (define (open-input-process args)
+    ;; Run a command, return a textual input port connected to its stdout.
+    ;; Closing the port waits for the child to exit.
+    (let* ([cmd (build-command-string args)])
+      (let-values ([(to-stdin from-stdout from-stderr pid)
+                    (open-process-ports cmd 'line (native-transcoder))])
+        (close-port to-stdin)
+        (close-port from-stderr)
+        ;; Wrap stdout in a custom port that tracks the pid
+        (let ([pp (make-process-port-rec #f from-stdout #f pid #f)])
+          ;; Return the stdout port directly — callers read from it
+          ;; Attach process-port to port via port-name convention
+          from-stdout))))
+
+  (define (open-output-process args)
+    ;; Run a command, return a textual output port connected to its stdin.
+    (let* ([cmd (build-command-string args)])
+      (let-values ([(to-stdin from-stdout from-stderr pid)
+                    (open-process-ports cmd 'line (native-transcoder))])
+        (close-port from-stdout)
+        (close-port from-stderr)
+        to-stdin)))
+
+  (define (open-process args)
+    ;; Run a command, return a process-port record with stdin/stdout/stderr.
+    ;; This is the full Gambit open-process equivalent.
+    (let* ([cmd (build-command-string args)])
+      (let-values ([(to-stdin from-stdout from-stderr pid)
+                    (open-process-ports cmd 'line (native-transcoder))])
+        (make-process-port-rec to-stdin from-stdout from-stderr pid #f))))
+
   ) ;; end library
diff --git a/lib/std/misc/rwlock.sls b/lib/std/misc/rwlock.sls
new file mode 100644
index 0000000..d01aea6
--- /dev/null
+++ b/lib/std/misc/rwlock.sls
@@ -0,0 +1,86 @@
+#!chezscheme
+;;; :std/misc/rwlock -- Read-write locks
+;;;
+;;; Concurrent readers, exclusive writers. Multiple threads can hold
+;;; the read lock simultaneously, but the write lock is exclusive.
+;;;
+;;; (define lock (make-rwlock))
+;;; (with-read-lock lock (lambda () (read-shared-data)))
+;;; (with-write-lock lock (lambda () (update-shared-data!)))
+
+(library (std misc rwlock)
+  (export make-rwlock rwlock?
+          read-lock! read-unlock!
+          write-lock! write-unlock!
+          with-read-lock with-write-lock)
+
+  (import (chezscheme))
+
+  (define-record-type rwlock
+    (fields
+      (immutable mtx)           ;; mutex protecting state
+      (immutable read-ok)       ;; condition: readers can proceed
+      (immutable write-ok)      ;; condition: writer can proceed
+      (mutable readers)         ;; number of active readers
+      (mutable writer?)         ;; #t if a writer holds the lock
+      (mutable waiting-writers));; number of writers waiting
+    (protocol
+      (lambda (new)
+        (lambda ()
+          (new (make-mutex) (make-condition) (make-condition) 0 #f 0)))))
+
+  (define (read-lock! rw)
+    (let ([m (rwlock-mtx rw)])
+      (mutex-acquire m)
+      ;; Wait while a writer holds or writers are waiting (writer preference)
+      (let lp ()
+        (when (or (rwlock-writer? rw) (> (rwlock-waiting-writers rw) 0))
+          (condition-wait (rwlock-read-ok rw) m)
+          (lp)))
+      (rwlock-readers-set! rw (+ (rwlock-readers rw) 1))
+      (mutex-release m)))
+
+  (define (read-unlock! rw)
+    (let ([m (rwlock-mtx rw)])
+      (mutex-acquire m)
+      (rwlock-readers-set! rw (- (rwlock-readers rw) 1))
+      (when (= (rwlock-readers rw) 0)
+        ;; Last reader out — wake a waiting writer
+        (condition-signal (rwlock-write-ok rw)))
+      (mutex-release m)))
+
+  (define (write-lock! rw)
+    (let ([m (rwlock-mtx rw)])
+      (mutex-acquire m)
+      (rwlock-waiting-writers-set! rw (+ (rwlock-waiting-writers rw) 1))
+      ;; Wait until no readers and no writer
+      (let lp ()
+        (when (or (rwlock-writer? rw) (> (rwlock-readers rw) 0))
+          (condition-wait (rwlock-write-ok rw) m)
+          (lp)))
+      (rwlock-waiting-writers-set! rw (- (rwlock-waiting-writers rw) 1))
+      (rwlock-writer?-set! rw #t)
+      (mutex-release m)))
+
+  (define (write-unlock! rw)
+    (let ([m (rwlock-mtx rw)])
+      (mutex-acquire m)
+      (rwlock-writer?-set! rw #f)
+      ;; Wake all waiting readers and one waiting writer
+      (condition-broadcast (rwlock-read-ok rw))
+      (condition-signal (rwlock-write-ok rw))
+      (mutex-release m)))
+
+  (define (with-read-lock rw thunk)
+    (dynamic-wind
+      (lambda () (read-lock! rw))
+      thunk
+      (lambda () (read-unlock! rw))))
+
+  (define (with-write-lock rw thunk)
+    (dynamic-wind
+      (lambda () (write-lock! rw))
+      thunk
+      (lambda () (write-unlock! rw))))
+
+  ) ;; end library
diff --git a/lib/std/misc/thread.sls b/lib/std/misc/thread.sls
index 2f61cff..a8b8919 100644
--- a/lib/std/misc/thread.sls
+++ b/lib/std/misc/thread.sls
@@ -18,6 +18,9 @@
 
 (library (std misc thread)
   (export
+    ;; Gerbil-compatible spawn (starts immediately, returns thread)
+    spawn spawn/name spawn/group
+
     ;; Thread operations
     make-thread thread-start! thread-join!
     thread-yield! thread-sleep!
@@ -295,4 +298,15 @@
            (mutex-release mx)
            #f]))))
 
+  ;;;; Gerbil-compatible spawn — create and start thread in one call
+
+  (define (spawn thunk)
+    (thread-start! (make-thread thunk)))
+
+  (define (spawn/name name thunk)
+    (thread-start! (make-thread thunk name)))
+
+  (define (spawn/group group thunk)
+    (thread-start! (make-thread thunk group)))
+
   ) ;; end library
diff --git a/lib/std/net/tcp.sls b/lib/std/net/tcp.sls
new file mode 100644
index 0000000..30c45a9
--- /dev/null
+++ b/lib/std/net/tcp.sls
@@ -0,0 +1,204 @@
+#!chezscheme
+;;; :std/net/tcp -- TCP client and server sockets
+;;;
+;;; Provides Gerbil-compatible TCP networking:
+;;;   (tcp-listen address port)       → server socket
+;;;   (tcp-accept server)             → (values input-port output-port)
+;;;   (tcp-connect address port)      → (values input-port output-port)
+;;;   (tcp-close server)              → void
+;;;
+;;; Ports are standard Chez Scheme binary ports transcoded to UTF-8.
+;;; Use with-tcp-server for automatic cleanup.
+
+(library (std net tcp)
+  (export
+    tcp-listen tcp-accept tcp-close
+    tcp-connect
+    tcp-server? tcp-server-port
+    with-tcp-server)
+
+  (import (chezscheme))
+
+  ;; ========== FFI ==========
+
+  ;; Load libc for POSIX socket functions
+  (define load-libc (load-shared-object #f))
+
+  (define c-socket    (foreign-procedure "socket" (int int int) int))
+  (define c-bind      (foreign-procedure "bind" (int void* int) int))
+  (define c-listen    (foreign-procedure "listen" (int int) int))
+  (define c-accept    (foreign-procedure "accept" (int void* void*) int))
+  (define c-connect   (foreign-procedure "connect" (int void* int) int))
+  (define c-close     (foreign-procedure "close" (int) int))
+  (define c-setsockopt (foreign-procedure "setsockopt" (int int int void* int) int))
+  (define c-read      (foreign-procedure "read" (int u8* size_t) ssize_t))
+  (define c-write     (foreign-procedure "write" (int u8* size_t) ssize_t))
+  (define c-htons     (foreign-procedure "htons" (unsigned-short) unsigned-short))
+  (define c-inet-pton (foreign-procedure "inet_pton" (int string void*) int))
+  (define c-getsockname (foreign-procedure "getsockname" (int void* void*) int))
+
+  ;; Constants
+  (define AF_INET 2)
+  (define SOCK_STREAM 1)
+  (define SOL_SOCKET 1)
+  (define SO_REUSEADDR 2)
+  (define SOCKADDR_IN_SIZE 16)  ;; sizeof(struct sockaddr_in) on Linux
+
+  ;; ========== sockaddr_in helpers ==========
+
+  (define (make-sockaddr-in address port)
+    ;; Build a struct sockaddr_in in foreign memory
+    (let ([buf (foreign-alloc SOCKADDR_IN_SIZE)])
+      ;; Zero the struct
+      (let lp ([i 0])
+        (when (< i SOCKADDR_IN_SIZE)
+          (foreign-set! 'unsigned-8 buf i 0)
+          (lp (+ i 1))))
+      ;; sin_family = AF_INET (offset 0, 2 bytes)
+      (foreign-set! 'unsigned-short buf 0 AF_INET)
+      ;; sin_port = htons(port) (offset 2, 2 bytes)
+      (foreign-set! 'unsigned-short buf 2 (c-htons port))
+      ;; sin_addr (offset 4, 4 bytes) — parse address string
+      (let ([addr-ptr (+ buf 4)])
+        (when (= (c-inet-pton AF_INET address addr-ptr) 0)
+          (foreign-free buf)
+          (error 'make-sockaddr-in "invalid address" address)))
+      buf))
+
+  (define (sockaddr-in-port buf)
+    ;; Extract port from sockaddr_in (in host byte order)
+    ;; sin_port is at offset 2, in network byte order
+    (let ([hi (foreign-ref 'unsigned-8 buf 2)]
+          [lo (foreign-ref 'unsigned-8 buf 3)])
+      (+ (* hi 256) lo)))
+
+  ;; ========== TCP Server ==========
+
+  (define-record-type tcp-server
+    (fields
+      (immutable fd)
+      (mutable port-num))   ;; actual port (useful when 0 = OS-assigned)
+    (sealed #t))
+
+  (define tcp-listen
+    (case-lambda
+      [(address port)
+       (tcp-listen address port 128)]
+      [(address port backlog)
+       (let ([fd (c-socket AF_INET SOCK_STREAM 0)])
+         (when (< fd 0)
+           (error 'tcp-listen "socket() failed"))
+         ;; SO_REUSEADDR
+         (let ([one (foreign-alloc 4)])
+           (foreign-set! 'int one 0 1)
+           (c-setsockopt fd SOL_SOCKET SO_REUSEADDR one 4)
+           (foreign-free one))
+         ;; Bind
+         (let ([addr (make-sockaddr-in address port)])
+           (let ([rc (c-bind fd addr SOCKADDR_IN_SIZE)])
+             (foreign-free addr)
+             (when (< rc 0)
+               (c-close fd)
+               (error 'tcp-listen "bind() failed" address port))))
+         ;; Listen
+         (when (< (c-listen fd backlog) 0)
+           (c-close fd)
+           (error 'tcp-listen "listen() failed"))
+         ;; Get actual port (important when port=0)
+         (let ([actual-port
+                (let ([buf (foreign-alloc SOCKADDR_IN_SIZE)]
+                      [len (foreign-alloc 4)])
+                  (foreign-set! 'int len 0 SOCKADDR_IN_SIZE)
+                  (c-getsockname fd buf len)
+                  (let ([p (sockaddr-in-port buf)])
+                    (foreign-free buf)
+                    (foreign-free len)
+                    p))])
+           (make-tcp-server fd actual-port)))]))
+
+  (define (tcp-server-port srv)
+    (tcp-server-port-num srv))
+
+  (define (tcp-accept srv)
+    ;; Accept a connection. Returns (values input-port output-port).
+    (let ([client-fd (c-accept (tcp-server-fd srv) 0 0)])
+      (when (< client-fd 0)
+        (error 'tcp-accept "accept() failed"))
+      (fd->ports client-fd "tcp-client")))
+
+  (define (tcp-close srv)
+    (c-close (tcp-server-fd srv)))
+
+  ;; ========== TCP Client ==========
+
+  (define (tcp-connect address port)
+    ;; Connect to a TCP server. Returns (values input-port output-port).
+    (let ([fd (c-socket AF_INET SOCK_STREAM 0)])
+      (when (< fd 0)
+        (error 'tcp-connect "socket() failed"))
+      (let ([addr (make-sockaddr-in address port)])
+        (let ([rc (c-connect fd addr SOCKADDR_IN_SIZE)])
+          (foreign-free addr)
+          (when (< rc 0)
+            (c-close fd)
+            (error 'tcp-connect "connect() failed" address port))))
+      (fd->ports fd "tcp-connection")))
+
+  ;; ========== Convenience ==========
+
+  (define-syntax with-tcp-server
+    (syntax-rules ()
+      [(_ (var address port) body body* ...)
+       (let ([var (tcp-listen address port)])
+         (dynamic-wind
+           (lambda () (void))
+           (lambda () body body* ...)
+           (lambda () (tcp-close var))))]))
+
+  ;; ========== Internal: FD → Ports ==========
+
+  (define (fd->ports fd name)
+    ;; Wrap a socket FD as a pair of transcoded text ports.
+    (let ([in (make-custom-binary-input-port
+                (string-append name "-in")
+                (lambda (bv start count)
+                  ;; read callback
+                  (let ([buf (make-bytevector count)])
+                    (let ([n (c-read fd buf count)])
+                      (if (<= n 0)
+                        0  ;; EOF
+                        (begin
+                          (bytevector-copy! buf 0 bv start n)
+                          n)))))
+                #f  ;; get-position
+                #f  ;; set-position!
+                (lambda () (c-close fd)))]
+          [out (make-custom-binary-output-port
+                 (string-append name "-out")
+                 (lambda (bv start count)
+                   ;; write callback
+                   (let ([buf (make-bytevector count)])
+                     (bytevector-copy! bv start buf 0 count)
+                     (let lp ([written 0])
+                       (if (= written count)
+                         count
+                         (let ([n (c-write fd
+                                    (let ([tmp (make-bytevector (- count written))])
+                                      (bytevector-copy! buf written tmp 0 (- count written))
+                                      tmp)
+                                    (- count written))])
+                           (if (<= n 0)
+                             written
+                             (lp (+ written n))))))))
+                 #f  ;; get-position
+                 #f  ;; set-position!
+                 #f)])  ;; don't double-close
+      (values
+        (transcoded-port in (make-transcoder (utf-8-codec)
+                              (eol-style none)
+                              (error-handling-mode replace)))
+        (transcoded-port out (make-transcoder (utf-8-codec)
+                               (eol-style none)
+                               (error-handling-mode replace))))))
+
+  ) ;; end library
diff --git a/lib/std/sugar.sls b/lib/std/sugar.sls
index dde870a..052e019 100644
--- a/lib/std/sugar.sls
+++ b/lib/std/sugar.sls
@@ -10,7 +10,8 @@
     let-hash
     defrule defrules
     chain chain-and with-id
-    assert!)
+    assert!
+    with-lock)
   (import (except (chezscheme)
             make-hash-table hash-table? iota 1+ 1-)
           (jerboa core))
@@ -96,4 +97,14 @@
          (lambda () body)
          (lambda () cleanup ...))]))
 
+  ;; with-lock — acquire Chez mutex, run body, release even on exception
+  (define-syntax with-lock
+    (syntax-rules ()
+      [(_ mutex-expr body body* ...)
+       (let ([m mutex-expr])
+         (dynamic-wind
+           (lambda () (mutex-acquire m))
+           (lambda () body body* ...)
+           (lambda () (mutex-release m))))]))
+
   ) ;; end library
diff --git a/tests/test-phase7.ss b/tests/test-phase7.ss
new file mode 100644
index 0000000..e18c54e
--- /dev/null
+++ b/tests/test-phase7.ss
@@ -0,0 +1,419 @@
+#!chezscheme
+;;; test-phase7.ss — Functional tests for Phase 7: Gerbil Porting Features
+;;;
+;;; Tests: spawn, atoms, rwlocks, TCP, process ports, with-lock
+
+(import (except (chezscheme) thread? atom?)
+        (only (std misc thread)
+              spawn spawn/name spawn/group
+              make-thread thread-start! thread-join!
+              thread-yield! thread-sleep! thread-name thread?)
+        (std misc atom)
+        (std misc rwlock)
+        (std net tcp)
+        (std misc process)
+        (std sugar))
+
+(define pass-count 0)
+(define fail-count 0)
+
+(define-syntax check
+  (syntax-rules (=>)
+    [(_ expr => expected)
+     (let ([result expr]
+           [exp expected])
+       (if (equal? result exp)
+         (set! pass-count (+ pass-count 1))
+         (begin
+           (set! fail-count (+ fail-count 1))
+           (display "FAIL: ")
+           (write 'expr)
+           (display " => ")
+           (write result)
+           (display " expected ")
+           (write exp)
+           (newline))))]
+    [(_ expr)
+     (if expr
+       (set! pass-count (+ pass-count 1))
+       (begin
+         (set! fail-count (+ fail-count 1))
+         (display "FAIL: ")
+         (write 'expr)
+         (display " => #f")
+         (newline)))]))
+
+(define-syntax check-no-error
+  (syntax-rules ()
+    [(_ expr)
+     (guard (e [#t
+                (set! fail-count (+ fail-count 1))
+                (display "FAIL (exception): ")
+                (write 'expr)
+                (display " => ")
+                (when (message-condition? e)
+                  (display (condition-message e)))
+                (newline)])
+       expr
+       (set! pass-count (+ pass-count 1)))]))
+
+
+;;; ======================================================================
+;;; Track 30: spawn / spawn/name / spawn/group
+;;; ======================================================================
+(display "--- Track 30: spawn ---\n")
+
+;; 30a. spawn creates a running thread that returns a value
+(let ([t (spawn (lambda () (+ 1 2 3)))])
+  (check (thread? t))
+  (check (thread-join! t) => 6))
+
+;; 30b. spawn/name sets the thread name
+(let ([t (spawn/name "worker-1" (lambda () 'done))])
+  (check (thread? t))
+  (check (equal? (thread-name t) "worker-1"))
+  (check (thread-join! t) => 'done))
+
+;; 30c. spawn/group works the same (name is the group)
+(let ([t (spawn/group "pool-1" (lambda () (* 7 6)))])
+  (check (thread? t))
+  (check (thread-join! t) => 42))
+
+;; 30d. spawn runs concurrently — two threads increment a shared counter
+(let ([counter (make-mutex)]
+      [total 0])
+  (define (inc-n n)
+    (lambda ()
+      (let lp ([i 0])
+        (when (< i n)
+          (with-mutex counter
+            (set! total (+ total 1)))
+          (lp (+ i 1))))))
+  (let ([t1 (spawn (inc-n 1000))]
+        [t2 (spawn (inc-n 1000))])
+    (thread-join! t1)
+    (thread-join! t2))
+  (check (= total 2000)))
+
+;; 30e. spawn catches exceptions
+(let ([t (spawn (lambda () (error 'test "boom")))])
+  (guard (e [#t (check (message-condition? e))])
+    (thread-join! t)
+    (check #f)))  ;; should not reach
+
+
+;;; ======================================================================
+;;; Track 31: Atoms
+;;; ======================================================================
+(display "--- Track 31: Atoms ---\n")
+
+;; 31a. Basic atom operations
+(let ([a (atom 42)])
+  (check (atom? a))
+  (check (atom-deref a) => 42)
+  (atom-reset! a 100)
+  (check (atom-deref a) => 100))
+
+;; 31b. atom-swap! applies function atomically
+(let ([a (atom 0)])
+  (atom-swap! a (lambda (x) (+ x 10)))
+  (check (atom-deref a) => 10)
+  (atom-swap! a (lambda (x) (* x 3)))
+  (check (atom-deref a) => 30))
+
+;; 31c. atom-update! with extra args
+(let ([a (atom 5)])
+  (atom-update! a + 10)
+  (check (atom-deref a) => 15)
+  (atom-update! a * 2)
+  (check (atom-deref a) => 30))
+
+;; 31d. atom is thread-safe — concurrent increments
+(let ([a (atom 0)])
+  (let ([t1 (spawn (lambda ()
+                      (let lp ([i 0])
+                        (when (< i 1000)
+                          (atom-swap! a (lambda (x) (+ x 1)))
+                          (lp (+ i 1))))))]
+        [t2 (spawn (lambda ()
+                      (let lp ([i 0])
+                        (when (< i 1000)
+                          (atom-swap! a (lambda (x) (+ x 1)))
+                          (lp (+ i 1))))))])
+    (thread-join! t1)
+    (thread-join! t2))
+  (check (atom-deref a) => 2000))
+
+;; 31e. atom with complex values
+(let ([a (atom '())])
+  (atom-swap! a (lambda (lst) (cons 'a lst)))
+  (atom-swap! a (lambda (lst) (cons 'b lst)))
+  (atom-swap! a (lambda (lst) (cons 'c lst)))
+  (check (atom-deref a) => '(c b a)))
+
+;; 31f. atom? predicate
+(check (not (atom? 42)))
+(check (not (atom? "hello")))
+(check (not (atom? (make-mutex))))
+(check (atom? (atom #f)))
+
+
+;;; ======================================================================
+;;; Track 32: Read-Write Locks
+;;; ======================================================================
+(display "--- Track 32: RWLock ---\n")
+
+;; 32a. Basic rwlock operations
+(let ([rw (make-rwlock)])
+  (check (rwlock? rw))
+  ;; Read lock / unlock
+  (read-lock! rw)
+  (read-unlock! rw)
+  ;; Write lock / unlock
+  (write-lock! rw)
+  (write-unlock! rw)
+  (check #t))
+
+;; 32b. with-read-lock / with-write-lock
+(let ([rw (make-rwlock)]
+      [data 0])
+  (with-write-lock rw (lambda () (set! data 42)))
+  (check (with-read-lock rw (lambda () data)) => 42))
+
+;; 32c. Multiple concurrent readers
+(let ([rw (make-rwlock)]
+      [data '(1 2 3 4 5)]
+      [results (make-vector 5 #f)])
+  ;; Launch 5 reader threads
+  (let ([threads
+         (let lp ([i 0] [ts '()])
+           (if (= i 5) ts
+             (lp (+ i 1)
+                 (cons (spawn (lambda ()
+                                (with-read-lock rw
+                                  (lambda ()
+                                    ;; All readers should see the same data
+                                    (thread-sleep! 0.01)  ;; hold read lock briefly
+                                    (length data)))))
+                       ts))))])
+    (for-each (lambda (t)
+                (let ([r (thread-join! t)])
+                  (check (= r 5))))
+              threads)))
+
+;; 32d. Writer excludes readers
+(let ([rw (make-rwlock)]
+      [shared 0])
+  ;; Writer sets shared to 42
+  (let ([writer (spawn (lambda ()
+                          (with-write-lock rw
+                            (lambda ()
+                              (set! shared 42)
+                              (thread-sleep! 0.01)))))])
+    ;; Small delay to let writer acquire lock
+    (thread-sleep! 0.005)
+    ;; Reader should see 42 after writer releases
+    (let ([reader (spawn (lambda ()
+                           (with-read-lock rw (lambda () shared))))])
+      (thread-join! writer)
+      (check (thread-join! reader) => 42))))
+
+;; 32e. with-write-lock cleans up on exception
+(let ([rw (make-rwlock)])
+  (guard (e [#t (void)])
+    (with-write-lock rw (lambda () (error 'test "boom"))))
+  ;; Lock should be released — another write-lock should succeed
+  (with-write-lock rw (lambda () (check #t))))
+
+
+;;; ======================================================================
+;;; Track 33: TCP Server
+;;; ======================================================================
+(display "--- Track 33: TCP ---\n")
+
+;; 33a. tcp-listen creates a server
+(let ([server (tcp-listen "127.0.0.1" 0)])  ;; port 0 = OS assigns
+  (check (tcp-server? server))
+  (check (> (tcp-server-port server) 0))
+  (tcp-close server))
+
+;; 33b. TCP client-server round-trip
+(let ([server (tcp-listen "127.0.0.1" 0)])
+  (let ([port (tcp-server-port server)])
+    ;; Server thread: accept one connection, echo back
+    (let ([server-thread
+           (spawn (lambda ()
+                    (let-values ([(in out) (tcp-accept server)])
+                      (let ([line (get-line in)])
+                        (put-string out (string-append "echo:" line "\n"))
+                        (flush-output-port out))
+                      (close-port in)
+                      (close-port out))))])
+      ;; Client: connect, send, receive
+      (let-values ([(in out) (tcp-connect "127.0.0.1" port)])
+        (put-string out "hello\n")
+        (flush-output-port out)
+        (let ([response (get-line in)])
+          (check (string=? response "echo:hello")))
+        (close-port in)
+        (close-port out))
+      (thread-join! server-thread)))
+  (tcp-close server))
+
+;; 33c. Multiple sequential connections
+(let ([server (tcp-listen "127.0.0.1" 0)])
+  (let ([port (tcp-server-port server)])
+    (let ([server-thread
+           (spawn (lambda ()
+                    (let lp ([i 0])
+                      (when (< i 3)
+                        (let-values ([(in out) (tcp-accept server)])
+                          (let ([line (get-line in)])
+                            (put-string out (string-append (number->string i) ":" line "\n"))
+                            (flush-output-port out))
+                          (close-port in)
+                          (close-port out))
+                        (lp (+ i 1))))))])
+      (let lp ([i 0])
+        (when (< i 3)
+          (let-values ([(in out) (tcp-connect "127.0.0.1" port)])
+            (put-string out (string-append "msg" (number->string i) "\n"))
+            (flush-output-port out)
+            (let ([response (get-line in)])
+              (check (string=? response
+                       (string-append (number->string i) ":msg" (number->string i)))))
+            (close-port in)
+            (close-port out))
+          (lp (+ i 1))))
+      (thread-join! server-thread)))
+  (tcp-close server))
+
+;; 33d. with-tcp-server auto-cleanup
+(let ([port-num #f])
+  (with-tcp-server (srv "127.0.0.1" 0)
+    (set! port-num (tcp-server-port srv))
+    (check (> port-num 0)))
+  ;; Server should be closed — connecting should fail
+  (guard (e [#t (check #t)])
+    (tcp-connect "127.0.0.1" port-num)
+    ;; If connect succeeds, the port might have been reused by OS