training data: rewrite jerboa-* URLs to sr.ht ~lisp

ober

118c70356caa660d6c796f81c1e2830c4801c3f9

diff --git a/cpt_corpus.jsonl b/cpt_corpus.jsonl
index 4ab20ad..48f03ff 100644
--- a/cpt_corpus.jsonl
+++ b/cpt_corpus.jsonl
@@ -341,7 +341,7 @@
 {"text": "<!-- FILE: docs/single-binary.md -->\n# Building a Self-Contained Chez Scheme Binary: Tricks and Techniques\n\nThis document captures the techniques for building a single ELF binary that embeds Chez Scheme's runtime, boot files, compiled modules, and FFI bindings. The binary works from any directory with zero external dependencies (beyond libc and system libraries).\n\n---\n\n## Table of Contents\n\n1. [Architecture Overview](#1-architecture-overview)\n2. [Embedding Boot Files as C Byte Arrays](#2-embedding-boot-files-as-c-byte-arrays)\n3. [The Threading Bug: Separating Libraries from Program](#3-the-threading-bug-separating-libraries-from-program)\n4. [Loading the Program via memfd](#4-loading-the-program-via-memfd)\n5. [Custom C Main: Bypassing Chez's Argument Parsing](#5-custom-c-main-bypassing-chezs-argument-parsing)\n6. [The Jerboa Compilation Pipeline](#6-the-jerboa-compilation-pipeline)\n7. [Import Conflict Resolution (R6RS Constraints)](#7-import-conflict-resolution-r6rs-constraints)\n8. [Mutable Export Workaround (identifier-syntax)](#8-mutable-export-workaround-identifier-syntax)\n9. [Chez Lazy Library Invocation Workaround](#9-chez-lazy-library-invocation-workaround)\n10. [Post-Build Patching](#10-post-build-patching)\n11. [The Gambit-to-Chez Compatibility Layer](#11-the-gambit-to-chez-compatibility-layer)\n12. [FFI via C Shim (No Foreign Library Dependencies)](#12-ffi-via-c-shim-no-foreign-library-dependencies)\n13. [WPO Incompatibility with identifier-syntax](#13-wpo-incompatibility-with-identifier-syntax)\n14. [Boot File Dependency Ordering](#14-boot-file-dependency-ordering)\n15. [Build Pipeline Summary](#15-build-pipeline-summary)\n\n---\n\n## 1. Architecture Overview\n\nThe binary packs four layers into one ELF:\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  jsh-main.c (custom C entry point)       \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  petite.boot  \u2500\u2510                         \u2502\n\u2502  scheme.boot   \u251c\u2500 C byte arrays          \u2502\n\u2502  jsh.boot     \u2500\u2518  (Sregister_boot_file_bytes) \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  jsh_program.h  (compiled program .so    \u2502\n\u2502                   as C byte array,       \u2502\n\u2502                   loaded via memfd)       \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  ffi-shim.c  (POSIX C bindings)         \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  libkernel  (Chez Scheme runtime, linked)\u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\nThe key insight: everything that would normally be separate files (boot files, compiled .so modules, the program itself) is converted to C byte arrays at build time and compiled directly into the binary.\n\n---\n\n## 2. Embedding Boot Files as C Byte Arrays\n\n**Problem**: Chez Scheme normally locates boot files by searching paths relative to the executable name. If you rename or move the binary, it can't find them. The traditional workaround \u2014 appending boot files to the ELF with `cat` \u2014 still depends on the binary name matching the boot file name (e.g., `jsh` needs `jsh.boot`).\n\n**Solution**: Use `Sregister_boot_file_bytes()` to register boot file contents from memory. At build time, each `.boot` file is serialized into a C header:\n\n```scheme\n;; build-binary.ss \u2014 generates C headers from binary files\n(define (file->c-header input-path output-path array-name size-name)\n  (let* ((data (get-bytevector-all (open-file-input-port input-path)))\n         (size (bytevector-length data)))\n    (call-with-output-file output-path\n      (lambda (out)\n        (fprintf out \"static const unsigned char ~a[] = {~n\" array-name)\n        (let loop ((i 0))\n          (when (< i size)\n            (fprintf out \"0x~2,'0x\" (bytevector-u8-ref data i))\n            ...))\n        (fprintf out \"static const unsigned int ~a = ~a;~n\" size-name size)))))\n```\n\nThis produces headers like:\n```c\n// jsh_petite_boot.h \u2014 ~1.9 MB as C array\nstatic const unsigned char petite_boot_data[] = {\n  0x00,0x00,0x00,0x00,0x63,0x68,0x65,0x7a,...\n};\nstatic const unsigned int petite_boot_size = 1961464;\n```\n\nAt startup, the C main registers them:\n```c\nSregister_boot_file_bytes(\"petite\", (void*)petite_boot_data, petite_boot_size);\nSregister_boot_file_bytes(\"scheme\", (void*)scheme_boot_data, scheme_boot_size);\nSregister_boot_file_bytes(\"jsh\",    (void*)jsh_boot_data,    jsh_boot_size);\nSbuild_heap(NULL, NULL);  // NULL \u2014 no need to search the filesystem\n```\n\nThe boot files total ~5.5 MB (petite ~1.9 MB, scheme ~1.0 MB, jsh ~2.6 MB), all compiled into the binary's `.rodata` section.\n\n---\n\n## 3. The Threading Bug: Separating Libraries from Program\n\n**Problem**: When a Chez Scheme program is included in a boot file via `make-boot-file`, any threads created by `fork-thread` will deadlock. The child threads block forever on an internal GC futex. This is a Chez Scheme bug.\n\n**Discovery**: The shell uses threads for pipeline parallelism and background jobs. Including the program in the boot file caused every pipeline to hang.\n\n**Solution**: Split the boot file into **libraries-only** (no program code). The program is compiled separately and loaded at runtime:\n\n```scheme\n;; Boot file contains ONLY libraries \u2014 no program\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n  (append\n    jerboa-runtime-modules    ; types, MOP, error, hash, compiler...\n    compat-layer-modules      ; gambit shims, misc, sugar, pregexp...\n    jsh-modules))             ; all shell modules in dependency order\n\n;; Program is compiled separately\n(compile-program \"jsh.ss\")    ; produces jsh.so\n```\n\nThe program `.so` is then embedded as a C byte array and loaded via `Sscheme_script` at runtime (see next section).\n\n---\n\n## 4. Loading the Program via memfd\n\n**Problem**: `Sscheme_script()` takes a file path, but the program `.so` is embedded in the binary \u2014 there's no file on disk.\n\n**Solution**: Use Linux's `memfd_create()` to create an anonymous in-memory file descriptor, write the embedded program data to it, and pass `/proc/self/fd/N` as the path:\n\n```c\n// Create anonymous memory-backed fd\nint fd = memfd_create(\"jsh-program\", MFD_CLOEXEC);\nwrite(fd, jsh_program_data, jsh_program_size);\n\n// Chez can load it via /proc/self/fd/N\nchar prog_path[64];\nsnprintf(prog_path, sizeof(prog_path), \"/proc/self/fd/%d\", fd);\n\n// Sbuild_heap loads libraries from boot files\nSbuild_heap(NULL, NULL);\n\n// Sscheme_script loads+runs the program (preserves threading!)\nint status = Sscheme_script(prog_path, 1, script_args);\n```\n\n**Why `Sscheme_script` instead of `Sscheme_start`**: `Sscheme_start` is for programs baked into boot files (which deadlock threads). `Sscheme_script` loads and runs a `.so` file after the heap is built, preserving full threading support.\n\n---\n\n## 5. Custom C Main: Bypassing Chez's Argument Parsing\n\n**Problem**: Chez Scheme's default `main()` interprets command-line flags. The shell's `-c` flag (run command string) is swallowed by Chez as `--compact` (heap compaction). Similar conflicts with `-e`, `-q`, etc.\n\n**Solution**: Write a custom `jsh-main.c` that saves all arguments in numbered environment variables before Chez sees them:\n\n```c\n// C main saves args in env vars\nsetenv(\"JSH_ARGC\", \"3\", 1);\nsetenv(\"JSH_ARG0\", \"-c\", 1);\nsetenv(\"JSH_ARG1\", \"echo hello\", 1);\nsetenv(\"JSH_ARG2\", \"world\", 1);\n\n// Chez is initialized with NO user args\nSscheme_init(NULL);\nSbuild_heap(NULL, NULL);\n```\n\nThe Scheme entry point reads them back:\n```scheme\n(define (get-real-args)\n  (let ((argc-str (getenv \"JSH_ARGC\")))\n    (if argc-str\n      ;; Binary mode: read from env vars\n      (let ((argc (string->number argc-str)))\n        (let loop ((i 0) (acc '()))\n          (if (>= i argc) (reverse acc)\n            (loop (+ i 1) (cons (getenv (format \"JSH_ARG~a\" i)) acc)))))\n      ;; Interpreted mode: use (command-line)\n      (cdr (command-line)))))\n```\n\nLink with the custom main instead of Chez's `main.o`:\n```\ngcc -rdynamic -o jsh jsh-main.o ffi-shim.o -lkernel -llz4 -lz -lm -ldl -lpthread -luuid -lncurses\n```\n\n---\n\n## 6. The Jerboa Compilation Pipeline\n\n**Problem**: Source files are written in Gerbil Scheme (`.ss`) which uses syntax, macros, and module system conventions incompatible with Chez's R6RS. The files must be preserved as `.ss` and only converted at build time.\n\n**Solution**: The Jerboa compiler (`gerbil-compile-to-library`) transforms each `.ss` file into an R6RS `(library ...)` form. An import map translates Gerbil module references to Chez library names:\n\n```scheme\n(define jsh-import-map\n  '((:std/sugar        . (compat sugar))     ; Gerbil sugar -> our compat lib\n    (:std/format       . (compat format))\n    (:std/pregexp      . (compat pregexp))\n    (:std/iter         . #f)                  ; stripped \u2014 Jerboa handles natively\n    (:std/foreign      . #f)                  ; stripped \u2014 not needed on Chez\n    (:gerbil/core      . #f)                  ; stripped\n    (\"./pregexp-compat\" . (jsh pregexp-compat)) ; relative import\n    ...))\n```\n\nBase imports provide the Gambit/Gerbil runtime compatibility layer that every module needs:\n```scheme\n(define jsh-base-imports\n  '((except (chezscheme) void box box? unbox set-box! ...)  ; exclude conflicts\n    (compat types)          ; Gerbil type system\n    (runtime util)          ; Gerbil runtime utilities\n    (runtime mop)           ; Meta-object protocol\n    (runtime error)         ; Gerbil error types\n    (runtime hash)          ; Hash tables\n    (compat gambit)         ; Gambit compatibility shims\n    (compat misc)))         ; String/list/path utilities\n```\n\nModules must be compiled in dependency order (7 tiers, from `ast` up to `main`).\n\n---\n\n## 7. Import Conflict Resolution (R6RS Constraints)\n\n**Problem**: R6RS forbids local definitions that shadow imported bindings. After Jerboa compilation, many modules define names that conflict with their imports (e.g., a module defines `find` which conflicts with `(compat misc)`'s `find`).\n\n**Solution**: A post-compilation pass (`fix-import-conflicts`) introspects each library's imports using `library-exports`, identifies conflicts with local definitions, and automatically adds `(except ...)` clauses:\n\n```scheme\n;; Before: bare import conflicts with local (define find ...)\n(import (compat misc))\n\n;; After: automatically patched\n(import (except (compat misc) find string-split))\n```\n\nThe algorithm also handles **import-vs-import** conflicts where two imported libraries export the same name \u2014 the later import gets an `except` clause for names already provided by earlier imports.\n\nAs a fallback when `library-exports` fails (library not yet loaded), the system reads exports directly from `.sls` files by parsing the `(library <name> (export ...) ...)` header.\n\n---\n\n## 8. Mutable Export Workaround (identifier-syntax)\n\n**Problem**: R6RS forbids `set!` on exported variables. Gerbil code frequently uses patterns like:\n\n```scheme\n(export *my-parameter*)\n(define *my-parameter* #f)\n...\n(set! *my-parameter* new-value)\n```\n\nThis is legal in Gerbil but illegal in R6RS.\n\n**Solution**: The build system scans for `(set! name ...)` where `name` is in the export list, then rewrites the definition to use `identifier-syntax` \u2014 a Chez macro that provides transparent get/set:\n\n```scheme\n;; Original (illegal in R6RS):\n(define *foo* initial-value)\n(set! *foo* new-value)        ; ERROR: exported variable\n\n;; Rewritten (transparent to all code):\n(define *foo*-cell (vector initial-value))\n(define-syntax *foo*\n  (identifier-syntax\n    (id (vector-ref *foo*-cell 0))\n    ((set! id v) (vector-set! *foo*-cell 0 v))))\n\n;; All existing (set! *foo* ...) and *foo* references work unchanged\n```\n\nThis transformation is invisible to all consuming code \u2014 reads and writes work exactly as before.\n\n---\n\n## 9. Chez Lazy Library Invocation Workaround\n\n**Problem**: Chez Scheme lazily invokes library bodies. A library's top-level expressions only run when a *runtime* export (not a macro) is first referenced. The `builtins.ss` module has side-effecting top-level expressions (`defbuiltin` calls) that register builtin commands into a global registry. If no code explicitly references a builtins export, these registrations never happen.\n\n**Solution**: Inject a dummy reference in `main.sls` that forces Chez to invoke `(jsh builtins)`:\n\n```scheme\n;; Patched into main.sls after Jerboa compilation:\n(define _force-builtins special-builtin?)  ; references a builtins export\n```\n\nThe entry point `jsh.ss` also forces it:\n```scheme\n(let () special-builtin? (void))  ; force (jsh builtins) invocation\n```\n\n---\n\n## 10. Post-Build Patching\n\nAfter Jerboa compilation, several patterns in the generated `.sls` files need fixing. These are applied as string-level patches on the generated code:\n\n### 10a. Keyword Dispatch in defclass Constructors\n\n**Bug**: Jerboa's `defclass` constructor translation incorrectly appends keyword argument *values* to the positional argument list. The generated `init!` method is a `case-lambda` dispatching on positional arity, so extra values cause the wrong clause to match.\n\n**Patch**:\n```scheme\n;; Before: keyword values added to positional args\n(lp (cddr rest) (cons (cadr rest) acc))\n\n;; After: skip keyword pairs entirely\n(lp (cddr rest) acc)\n```\n\nCallers that used keyword syntax are also patched to positional:\n```scheme\n;; Before:\n(make-shell-environment 'parent: env 'name: name)\n\n;; After:\n(make-shell-environment env name)\n```\n\n### 10b. Exception Messages\n\n**Bug**: Chez's `condition-message` returns raw format templates (e.g., `\"variable ~:s is not bound\"`) with `~:s` placeholders, unlike Gambit which returns formatted strings.\n\n**Patch**: Replace `exception-message` to use `display-condition` which formats the message with its irritants, then strip the `\"Exception: \"` or `\"Exception in <who>: \"` prefix.\n\n### 10c. make-mutex Argument Type\n\n**Bug**: Gambit's `make-mutex` accepts strings; Chez requires a symbol or `#f`.\n\n**Patch**: `(make-mutex \"pipeline-fd\")` -> `(make-mutex 'pipeline-fd)`\n\n### 10d. Recursive void Definition\n\n**Bug**: Gerbil's `void` is variadic (accepts any args, returns void). Jerboa translates `(define (void) (void))` which creates a 0-arg function that infinitely recurses.\n\n**Patch**: `(define (void) (void))` -> `(define (void . _) (if #f #f))`\n\n---\n\n## 11. The Gambit-to-Chez Compatibility Layer\n\nThe `src/compat/` directory provides ~900 lines of shims translating Gambit APIs to Chez equivalents. Key modules:\n\n### gambit.sls (~645 lines)\n- **u8vector/bytevector**: Gambit's `u8vector` API mapped to Chez bytevectors\n- **Threading**: `spawn`, `thread-join!`, `mutex-lock!`/`mutex-unlock!` -> Chez `fork-thread`, `mutex-acquire`/`mutex-release`\n- **Processes**: `open-process`, `process-status`, `process-pid` -> Chez `process` with pipe-based communication\n- **File system**: `file-info`, `file-type`, `directory-files` -> Chez equivalents with Gambit-compatible return types\n- **I/O**: `/dev/fd/N` support (Gambit feature), keyword argument handling for `open-input-file`/`open-output-file`\n- **tty-mode-set!**: Terminal raw mode via FFI\n\n### sugar.sls (~184 lines)\n- `def`, `def*`, `defrule` (Gerbil syntax macros)\n- `try`/`catch`/`finally`, `with-catch`, `unwind-protect`\n- `while`, `until` loops\n- `defstruct` record type definitions\n- Custom `error` function (Gerbil-style, no `who` parameter)\n\n### misc.sls (~200 lines)\n- String: `string-prefix?`, `string-suffix?`, `string-split`, `string-join`, `string-trim`, `string-contains`, `string-index`\n- List: `flatten`, `unique`, `every`, `any`, `iota`, `last`\n- Path: `path-expand`, `path-directory`, `path-extension`\n- Hash: `hash-ref` with default, `hash-copy`, `hash->list`\n\n### Runtime Shims in jsh.ss\n\nThe entry point installs additional Gambit primitives into the interaction environment for Gerbil eval:\n\n```scheme\n;; f64vector -> flvector\n(define (make-f64vector n . rest) (make-flvector n ...))\n\n;; Threading: vector-based thread objects with mutex+condition\n(define (make-thread thunk) (vector thunk (make-mutex) (make-condition) #f #f))\n(define (thread-start! thr) (fork-thread ...))\n(define (thread-join! thr) (mutex-acquire ...) (condition-wait ...))\n\n;; Gambit ##process-statistics -> flvector with cpu/real/gc times\n(define (##process-statistics) ...)\n\n;; SMP primitives -> no-ops\n(define (##set-parallelism-level! n) (void))\n```\n\n---\n\n## 12. FFI via C Shim (No Foreign Library Dependencies)\n\n**Problem**: The shell needs low-level POSIX operations (fork/exec, signal handling, terminal control, job control) that Chez doesn't expose directly.\n\n**Solution**: A single C file (`ffi-shim.c`, ~480 lines) provides all POSIX bindings. It's compiled as a shared library for interpreted mode and as an object file linked directly into the binary:\n\n- **Signal handling**: Flag-based async-signal-safe mechanism (`_signal_flags[]` + `sigaction`)\n- **Fork/exec**: `ffi_fork_exec()` handles fork, signal reset, fd cleanup, process groups, and execve in C (no Scheme in the child process)\n- **Argument passing**: SOH (0x01) delimited packed strings for argv/envp \u2014 avoids marshaling string arrays across FFI\n- **Pipe/fd management**: Cached pipe fds, bulk fd close via `/proc/self/fd`\n- **Terminal**: Raw mode, termios save/restore, window size via `ioctl(TIOCGWINSZ)`\n- **Read buffer**: 1 MB static buffer for `ffi_do_read_all()` \u2014 avoids Scheme-side allocation for command substitution\n\nAll functions use standard POSIX libc \u2014 zero external library dependencies for the FFI layer.\n\n---\n\n## 13. WPO Incompatibility with identifier-syntax\n\n**Problem**: Chez's Whole-Program Optimization (WPO) analyzes all code and eliminates dead definitions. The `identifier-syntax` macros generated for mutable exports (see section 8) appear to WPO as unused macro-only definitions \u2014 it eliminates the underlying vector-cell storage, breaking everything.\n\n**Solution**: Skip WPO entirely. Use the plain `jsh.so` from `compile-program` without running `compile-whole-program`:\n\n```scheme\n;; DO NOT use compile-whole-program \u2014 it eliminates identifier-syntax cells\n(compile-program \"jsh.ss\")    ; produces jsh.so\n(system \"cp jsh.so jsh-all.so\")  ; use directly, no WPO pass\n```\n\nThe binary size penalty is minimal (~10%) and avoids subtle runtime crashes.\n\n---\n\n## 14. Boot File Dependency Ordering\n\nThe `make-boot-file` call must list `.so` files in strict dependency order. A module can only reference definitions from modules listed before it. Example order:\n\n```\n1. Jerboa runtime (9 modules):\n   types -> gambit-compat -> util -> table -> c3 -> mop -> error -> hash\n   -> syntax -> eval -> reader -> compile -> jerboa\n\n2. Compat layer (9 modules):\n   gambit -> misc -> sort -> sugar -> format -> pregexp\n   -> signal -> signal-handler -> fdio\n\n3. JSH modules (30 modules, 7 tiers):\n   Tier 1: ffi, pregexp-compat, stage, static-compat\n   Tier 2: ast, registry, macros, util\n   Tier 3: environment, lexer, arithmetic, glob, fuzzy, history\n   Tier 4: parser, functions, signals, expander\n   Tier 5: redirect, control, jobs, builtins\n   Tier 6: pipeline, executor, completion, prompt\n   Tier 7: lineedit, fzf, script, startup, main\n```\n\nGetting this order wrong produces cryptic \"unbound variable\" errors at runtime.\n\n---\n\n## 15. Build Pipeline Summary\n\n```\nStep 1: gcc -> libjsh-ffi.so          (C FFI shim, shared lib for interpreted mode)\nStep 2: Jerboa -> src/jsh/*.sls       (30 .ss files -> 30 .sls R6RS libraries)\n        + post-build patches           (import conflicts, mutable exports, Chez compat)\nStep 3: Chez compile-program          (jsh.ss -> jsh.so, compiles all imported libs)\nStep 4: make-boot-file                (48 .so files -> jsh.boot, libs-only)\nStep 5: file->c-header x 4            (petite.boot, scheme.boot, jsh.boot, jsh.so -> .h)\nStep 6: gcc -> jsh-main.o             (custom main with embedded boot+program arrays)\n        gcc -> ffi-shim.o             (POSIX bindings)\nStep 7: gcc -> jsh                    (link with -lkernel -llz4 -lz -lm -ldl -lpthread -luuid -lncurses)\n\nResult: ./jsh  (~6.5 MB, fully self-contained ELF)\n```\n\n### What's Inside the Binary\n\n| Component | Size | Purpose |\n|-----------|------|---------|\n| petite.boot | ~1.9 MB | Chez base runtime (I/O, syntax, eval) |\n| scheme.boot | ~1.0 MB | Full Chez compiler |\n| jsh.boot | ~2.6 MB | Jerboa runtime + compat + 30 shell modules |\n| jsh program | ~9 KB | Entry point + Gerbil eval handler |\n| ffi-shim | ~15 KB | POSIX C bindings |\n| libkernel | ~1.0 MB | Chez Scheme runtime (GC, threads, FFI) |\n\nThe binary runs from any directory, under any name, with no external Chez/Gerbil/Gambit installation required.\n"}
 {"text": "<!-- FILE: docs/pattern-matching.md -->\n# Pattern Matching 2.0 \u2014 `(std match2)`\n\nThe `(std match2)` library extends Chez Scheme with a powerful pattern matching system. It builds on the ideas behind `(std match)` and adds three major capabilities: **sealed hierarchies** (closed sets of variants that enable exhaustiveness checking), **active patterns** (user-defined extractors that run arbitrary code during matching), and **view patterns** (computed projections bound as match variables). Together these make it practical to write exhaustive, structurally typed dispatch code in Chez Scheme.\n\n## Table of Contents\n\n1. [Quick Start](#quick-start)\n2. [Core API](#core-api)\n3. [Pattern Syntax Reference](#pattern-syntax-reference)\n4. [Sealed Hierarchies](#sealed-hierarchies)\n5. [Active Patterns](#active-patterns)\n6. [Guards](#guards)\n7. [Complete Examples](#complete-examples)\n8. [Exhaustiveness Checking with `match/strict`](#exhaustiveness-checking-with-matchstrict)\n9. [Comparison with Standard Match](#comparison-with-standard-match)\n\n---\n\n## Quick Start\n\n```scheme\n(import (chezscheme) (std match2))\n\n;; Basic structural matching\n(match '(1 2 3)\n  [(list a b c) (+ a b c)])   ; => 6\n\n;; Predicate patterns\n(match 42\n  [(? string?) 'string]\n  [(? number?) 'number])      ; => 'number\n\n;; Struct patterns (after registering the type)\n(define-record-type (point make-point point?)\n  (fields (immutable x point-x) (immutable y point-y)))\n\n(define-match-type point point? point-x point-y)\n\n(match (make-point 3 4)\n  [(point x y) (sqrt (+ (* x x) (* y y)))])  ; => 5.0\n```\n\n---\n\n## Core API\n\n### `match`\n\n```scheme\n(match expr clause ...)\n```\n\nEvaluates `expr` and tests each `clause` in order until one matches. A clause has the form `(pattern body ...)` or `(pattern (where guard) body ...)`. The value of the first matching clause's body is returned. If no clause matches, an error is raised.\n\n```scheme\n(match value\n  [pattern1 result1]\n  [pattern2 result2]\n  [_ default])\n```\n\n### `define-sealed-hierarchy`\n\n```scheme\n(define-sealed-hierarchy hier-name\n  (variant-name pred-fn accessor ...)\n  ...)\n```\n\nDeclares a closed set of variant types under a single hierarchy name. Each variant is registered for structural matching and the group is registered as a sealed hierarchy for `match/strict` coverage checking.\n\n- `hier-name` \u2014 symbol naming the hierarchy (used with `match/strict`)\n- `variant-name` \u2014 symbol used as the pattern head (e.g., `(variant-name field ...)`)\n- `pred-fn` \u2014 predicate procedure for this variant\n- `accessor ...` \u2014 field accessor procedures in order\n\n```scheme\n(define-sealed-hierarchy expr\n  (lit-expr lit-expr? lit-value)\n  (add-expr add-expr? add-left add-right)\n  (var-expr var-expr? var-name))\n```\n\n### `sealed-hierarchy?`\n\n```scheme\n(sealed-hierarchy? name) => boolean\n```\n\nReturns `#t` if `name` (a symbol) has been registered as a sealed hierarchy.\n\n```scheme\n(sealed-hierarchy? 'expr)    ; => #t\n(sealed-hierarchy? 'unknown) ; => #f\n```\n\n### `sealed-hierarchy-members`\n\n```scheme\n(sealed-hierarchy-members name) => list\n```\n\nReturns the member list of a sealed hierarchy. Each element is a list `(variant-name pred-fn accessor ...)`. Returns `'()` if the hierarchy is not registered.\n\n```scheme\n(sealed-hierarchy-members 'expr)\n; => ((lit-expr #<procedure lit-expr?> #<procedure lit-value>)\n;     (add-expr #<procedure add-expr?> ...)\n;     ...)\n```\n\n### `register-struct-type!`\n\n```scheme\n(register-struct-type! name pred accessor ...)\n```\n\nRegisters a struct type for structural matching by name. After registration, `(name field1 field2 ...)` can be used as a pattern in `match`. This is the runtime procedure that `define-match-type` and `define-sealed-hierarchy` call internally.\n\n```scheme\n(define-record-type (point make-point point?)\n  (fields (immutable x point-x) (immutable y point-y)))\n\n(register-struct-type! 'point point? point-x point-y)\n```\n\n### `define-match-type`\n\n```scheme\n(define-match-type type-name pred-fn accessor ...)\n```\n\nConvenience macro that calls `register-struct-type!`. Use this to make any record type usable as a pattern head.\n\n```scheme\n(define-match-type point point? point-x point-y)\n\n;; Now usable in patterns:\n(match (make-point 1 2)\n  [(point x y) (list x y)])  ; => '(1 2)\n```\n\n### `match/strict`\n\n```scheme\n(match/strict sealed-type expr clause ...)\n(match/strict expr clause ...)\n```\n\nLike `match`, but signals intent that all variants of a sealed hierarchy are handled. The `sealed-type` argument names the hierarchy being matched; it is available for documentation and tooling purposes. If no clause matches at runtime, an error is raised (same as `match`).\n\nNote: Full static exhaustiveness checking is not possible at Chez macro-expansion time under R6RS phasing. `match/strict` is primarily a documentation and convention signal \u2014 it pairs naturally with `define-sealed-hierarchy` to communicate \"this match is supposed to cover all cases.\"\n\n```scheme\n(match/strict shape s\n  [(circle r)   (* pi r r)]\n  [(rect   w h) (* w h)])\n```\n\n### `define-active-pattern`\n\n```scheme\n(define-active-pattern (name input) body ...)\n```\n\nDefines a named extractor function for use in `match` patterns. The body receives the value being matched as `input` and should return:\n\n- `#f` \u2014 pattern does not match\n- `#t` \u2014 pattern matches, no extracted values\n- A list `(v ...)` \u2014 pattern matches, values are bound to sub-patterns\n- A vector `#(v ...)` \u2014 same as list form\n\nActive patterns integrate seamlessly into `match` syntax: `(name sub-pat ...)` is valid wherever any other structural pattern is valid.\n\n```scheme\n(define-active-pattern (even? n)\n  (and (integer? n) (even? n)))\n\n(define-active-pattern (split-at-comma s)\n  (if (string? s)\n    (let ([i (string-contains s \",\")])\n      (if i\n        (list (substring s 0 i)\n              (substring s (+ i 1) (string-length s)))\n        #f))\n    #f))\n```\n\n### `active-pattern?`\n\n```scheme\n(active-pattern? name) => boolean\n```\n\nReturns `#t` if `name` (a symbol) has been registered via `define-active-pattern`.\n\n### `active-pattern-proc`\n\n```scheme\n(active-pattern-proc name) => procedure | #f\n```\n\nReturns the extractor procedure for an active pattern, or `#f` if not registered. Useful for introspection or testing active patterns directly.\n\n```scheme\n((active-pattern-proc 'split-at-comma) \"a,b\")  ; => (\"a\" \"b\")\n((active-pattern-proc 'split-at-comma) \"nope\")  ; => #f\n```\n\n---\n\n## Pattern Syntax Reference\n\n### Wildcard\n\n```scheme\n_\n```\n\nMatches any value without binding.\n\n```scheme\n(match x [_ 'anything])\n```\n\n### Variables\n\n```scheme\nname\n```\n\nAny identifier (other than `_` and reserved pattern keywords) binds the matched value.\n\n```scheme\n(match 42 [n (* n 2)])  ; => 84\n```\n\n### Literals\n\n| Pattern | Matches |\n|---------|---------|\n| `42` | Number equal to 42 |\n| `\"hello\"` | String equal to `\"hello\"` |\n| `#t` / `#f` | Boolean |\n| `'foo` | Symbol `foo` |\n| `'(1 2)` | Any datum equal under `equal?` |\n\n```scheme\n(match x\n  [0    \"zero\"]\n  [1    \"one\"]\n  [\"hi\" \"greeting\"]\n  ['ok  \"ok symbol\"])\n```\n\n### Predicate: `(? pred)`\n\n```scheme\n(? predicate)\n```\n\nMatches if `(predicate value)` is truthy. Does not bind the value.\n\n```scheme\n(match x\n  [(? number?) 'numeric]\n  [(? string?) 'textual])\n```\n\n### Predicate with Binding: `(? pred -> var)`\n\n```scheme\n(? predicate -> variable)\n```\n\nApplies `predicate` to the value. If the result is truthy, binds it to `variable` and succeeds. This is useful when the predicate returns a transformed value (e.g., `string->number`).\n\n```scheme\n(match \"42\"\n  [(? string->number -> n) (* n 10)]  ; => 420\n  [_ 'not-a-number])\n```\n\n### View Pattern: `(=> proc var)`\n\n```scheme\n(=> procedure variable)\n```\n\nApplies `procedure` to the value unconditionally and binds the result to `variable`. Always succeeds. Use this for projections or computed views of the input.\n\n```scheme\n(match \"hello\"\n  [(=> string-length len) len])  ; => 5\n\n(match 10\n  [(=> (lambda (x) (* x x)) sq) sq])  ; => 100\n```\n\n### Conjunction: `(and pat ...)`\n\n```scheme\n(and pattern ...)\n```\n\nMatches if all sub-patterns match. Bindings from all sub-patterns are in scope in the body. `(and)` with no sub-patterns always succeeds.\n\n```scheme\n(match x\n  [(and (? number?) (? positive?)) 'positive-number]\n  [(and (? number?) n)             (list 'nonpositive n)])\n```\n\n### Disjunction: `(or pat ...)`\n\n```scheme\n(or pattern ...)\n```\n\nMatches if any sub-pattern matches. Because different branches may bind different variables, bindings from `or` sub-patterns are not visible in the body \u2014 use `or` only for pure dispatch without variable capture. `(or)` with no sub-patterns never matches.\n\n```scheme\n(match x\n  [(or 1 2 3) 'small]\n  [_          'large])\n```\n\n### Negation: `(not pat)`\n\n```scheme\n(not pattern)\n```\n\nMatches if the sub-pattern does not match. No bindings are captured.\n\n```scheme\n(match x\n  [(not (? string?)) 'not-a-string]\n  [_                 'string])\n```\n\n### Pair: `(cons p1 p2)`\n\n```scheme\n(cons car-pattern cdr-pattern)\n```\n\nMatches a pair. Binds the car to `car-pattern` and the cdr to `cdr-pattern`.\n\n```scheme\n(match '(1 2 3)\n  [(cons h t) (list 'head h 'tail t)])  ; => '(head 1 tail (2 3))\n```\n\n### Exact List: `(list pat ...)`\n\n```scheme\n(list pattern ...)\n```\n\nMatches a proper list of exactly the given length. Each sub-pattern matches the corresponding element.\n\n```scheme\n(match '(x y z)\n  [(list a b c) (list c b a)])  ; => '(z y x)\n\n(match '()\n  [(list) 'empty])\n```\n\n### Improper / Prefix List: `(list* pat ... rest)`\n\n```scheme\n(list* pattern ... rest-pattern)\n```\n\nMatches a list with at least n elements (where n is the number of leading patterns). The leading patterns match elements 0 through n-1; `rest-pattern` matches the tail starting at element n.\n\n```scheme\n(match '(1 2 3 4 5)\n  [(list* a b rest) (list a b rest)])  ; => '(1 2 (3 4 5))\n```\n\n### Vector: `(vector pat ...)`\n\n```scheme\n(vector pattern ...)\n```\n\nMatches a vector of exactly the given length.\n\n```scheme\n(match (vector 10 20 30)\n  [(vector a b c) (+ a b c)])  ; => 60\n```\n\n### Box: `(box pat)`\n\n```scheme\n(box pattern)\n```\n\nMatches a Chez Scheme `box` and applies the sub-pattern to the unboxed value.\n\n```scheme\n(match (box 99)\n  [(box n) n])  ; => 99\n```\n\n### Struct / Named Pattern: `(TypeName pat ...)`\n\n```scheme\n(TypeName field-pattern ...)\n```\n\nMatches a struct value whose type was registered via `register-struct-type!`, `define-match-type`, or `define-sealed-hierarchy`. The sub-patterns are matched against the field values in registration order.\n\nWorks identically for both struct types and active patterns \u2014 the runtime dispatcher tries active patterns first, then struct types.\n\n```scheme\n(match (make-point 3 4)\n  [(point x y) (+ x y)])  ; => 7\n```\n\n### Pattern Guards: `(where expr)`\n\nA guard condition can appear as the first form after the pattern in a clause:\n\n```scheme\n(pattern (where guard-expression) body ...)\n```\n\nThe guard is evaluated with all pattern bindings in scope. If it returns `#f`, the clause is skipped and matching continues with the next clause.\n\n```scheme\n(match '(3 4)\n  [(list a b) (where (= a b)) 'equal]\n  [(list a b)                 'different])  ; => 'different\n\n(match 42\n  [n (where (> n 100)) 'big]\n  [n                   (list 'small n)])    ; => '(small 42)\n```\n\n---\n\n## Sealed Hierarchies\n\nA sealed hierarchy declares that a set of variants is closed \u2014 there are no other variants. This is the Jerboa equivalent of a sum type or tagged union.\n\n```scheme\n;; 1. Define the record types\n(define-record-type (circle make-circle circle?)\n  (fields (immutable radius circle-radius)))\n\n(define-record-type (rect make-rect rect?)\n  (fields (immutable w rect-w) (immutable h rect-h)))\n\n(define-record-type (triangle make-triangle triangle?)\n  (fields (immutable base triangle-base) (immutable height triangle-height)))\n\n;; 2. Declare the sealed hierarchy\n(define-sealed-hierarchy shape\n  (circle   circle?   circle-radius)\n  (rect     rect?     rect-w rect-h)\n  (triangle triangle? triangle-base triangle-height))\n\n;; 3. Match against variants\n(define (area s)\n  (match/strict shape s\n    [(circle   r)   (* 3.14159 r r)]\n    [(rect     w h) (* w h)]\n    [(triangle b h) (/ (* b h) 2.0)]))\n\n(area (make-circle 5))         ; => 78.53975\n(area (make-rect 3 4))         ; => 12\n(area (make-triangle 6 4))     ; => 12.0\n```\n\n`define-sealed-hierarchy` automatically calls `register-struct-type!` for each variant, so the variants are immediately usable in ordinary `match` as well.\n\nIntrospection:\n\n```scheme\n(sealed-hierarchy? 'shape)        ; => #t\n(length (sealed-hierarchy-members 'shape))  ; => 3\n```\n\n---\n\n## Active Patterns\n\nActive patterns let you define custom extractors \u2014 matching logic and value transformation in one place. They integrate into `match` syntax as named pattern heads.\n\n### Boolean Active Patterns\n\nReturn `#t` or `#f` for predicate-only tests:\n\n```scheme\n(define-active-pattern (even-int? n)\n  (and (integer? n) (even? n)))\n\n(match 4\n  [(even-int?) 'even]\n  [_           'odd])   ; => 'even\n```\n\n### Extracting Active Patterns\n\nReturn a list to provide values to sub-patterns:\n\n```scheme\n(define-active-pattern (double n)\n  (list (* n 2)))\n\n(match 5\n  [(double d) d])   ; => 10\n```\n\n### Multi-Value Extraction\n\n```scheme\n(define-active-pattern (parse-kv s)\n  ;; Splits \"key=value\" into two strings, or fails\n  (and (string? s)\n       (let loop ([i 0])\n         (cond\n           [(= i (string-length s)) #f]\n           [(char=? (string-ref s i) #\\=)\n            (list (substring s 0 i)\n                  (substring s (+ i 1) (string-length s)))]\n           [else (loop (+ i 1))]))))\n\n(match \"name=Alice\"\n  [(parse-kv k v) (list 'key k 'val v)]\n  [_              'no-equals])\n; => '(key \"name\" val \"Alice\")\n\n(match \"no-equals\"\n  [(parse-kv k v) (list k v)]\n  [_              'no-equals])\n; => 'no-equals\n```\n\n### Nesting Active Patterns\n\nActive patterns can be nested with other patterns:\n\n```scheme\n(define-active-pattern (abs-val n)\n  (list (if (negative? n) (- n) n)))\n\n(match -42\n  [(abs-val (? (lambda (x) (> x 10)))) 'large-magnitude]\n  [(abs-val a)                          (list 'magnitude a)])\n; => 'large-magnitude\n```\n\n---\n\n## Complete Examples\n\n### AST Processing with Sealed Hierarchy\n\n```scheme\n(import (chezscheme) (std match2))\n\n;; Define an expression AST\n(define-record-type (num-node  make-num  num?)\n  (fields (immutable value num-value)))\n(define-record-type (add-node  make-add  add?)\n  (fields (immutable left add-left) (immutable right add-right)))\n(define-record-type (mul-node  make-mul  mul?)\n  (fields (immutable left mul-left) (immutable right mul-right)))\n(define-record-type (var-node  make-var  var?)\n  (fields (immutable name var-name)))\n\n(define-sealed-hierarchy expr\n  (num-node num?  num-value)\n  (add-node add?  add-left add-right)\n  (mul-node mul?  mul-left mul-right)\n  (var-node var?  var-name))\n\n;; Evaluator\n(define (eval-expr node env)\n  (match/strict expr node\n    [(num-node v)   v]\n    [(add-node l r) (+ (eval-expr l env) (eval-expr r env))]\n    [(mul-node l r) (* (eval-expr l env) (eval-expr r env))]\n    [(var-node n)   (cdr (assq n env))]))\n\n(define my-env '((x . 3) (y . 4)))\n\n(eval-expr\n  (make-add (make-mul (make-var 'x) (make-var 'x))\n            (make-mul (make-var 'y) (make-var 'y)))\n  my-env)\n; => 25  (3*3 + 4*4)\n\n;; Pretty printer\n(define (pp-expr node)\n  (match/strict expr node\n    [(num-node v)   (number->string v)]\n    [(var-node n)   (symbol->string n)]\n    [(add-node l r) (string-append \"(\" (pp-expr l) \" + \" (pp-expr r) \")\")]\n    [(mul-node l r) (string-append \"(\" (pp-expr l) \" * \" (pp-expr r) \")\")]))\n\n(pp-expr (make-add (make-var 'x) (make-num 1)))\n; => \"(x + 1)\"\n```\n\n### JSON Value Matching\n\n```scheme\n(import (chezscheme) (std match2))\n\n;; JSON values are represented as: number, string, boolean, 'null,\n;; (vector ...) for arrays, and alists for objects.\n\n(define (json-type v)\n  (match v\n    [(? number?)  'number]\n    [(? string?)  'string]\n    [#t           'true]\n    [#f           'false]\n    ['null        'null]\n    [(? vector?)  'array]\n    [(? pair?)    'object]\n    [_            'unknown]))\n\n(define (json-get obj key)\n  (match obj\n    [(? pair?) (let ([entry (assoc key obj)])\n                 (if entry (cdr entry) 'null))]\n    [_         'null]))\n\n(define (json-summarize v)\n  (match v\n    [(? number?)              (format \"~a\" v)]\n    [(? string?)              (format \"\\\"~a\\\"\" v)]\n    [#t                       \"true\"]\n    [#f                       \"false\"]\n    ['null                    \"null\"]\n    [(? vector?)\n     (format \"[~a items]\" (vector-length v))]\n    [(list* (cons _ _) rest)\n     (format \"{~a keys}\" (+ 1 (length rest)))]\n    [_                        \"?\"]))\n\n(json-type 42)            ; => 'number\n(json-type \"hello\")       ; => 'string\n(json-type #t)            ; => 'true\n(json-type '((x . 1)))   ; => 'object\n(json-summarize '((name . \"Alice\") (age . 30)))  ; => \"{2 keys}\"\n```\n\n### Active Pattern for Parsing\n\n```scheme\n(import (chezscheme) (std match2))\n\n;; Active pattern: parse an ISO date string \"YYYY-MM-DD\"\n(define-active-pattern (iso-date s)\n  (and (string? s)\n       (= (string-length s) 10)\n       (char=? (string-ref s 4) #\\-)\n       (char=? (string-ref s 7) #\\-)\n       (let ([y (string->number (substring s 0 4))]\n             [m (string->number (substring s 5 7))]\n             [d (string->number (substring s 8 10))])\n         (and y m d (list y m d)))))\n\n(define (classify-date s)\n  (match s\n    [(iso-date y m d)\n     (cond\n       [(= m 1)  (format \"~a is in January\" y)]\n       [(= m 12) (format \"~a is in December\" y)]\n       [else     (format \"~a-~a-~a\" y m d)])]\n    [_ \"not a date\"]))\n\n(classify-date \"2024-01-15\")  ; => \"2024 is in January\"\n(classify-date \"2024-12-25\")  ; => \"2024 is in December\"\n(classify-date \"not-a-date\")  ; => \"not a date\"\n```\n\n### Exhaustive Match Catching Missing Cases\n\n```scheme\n(import (chezscheme) (std match2))\n\n(define-record-type (ok-val  make-ok  ok?)  (fields (immutable val ok-val)))\n(define-record-type (err-val make-err err?) (fields (immutable msg err-msg)))\n\n(define-sealed-hierarchy result\n  (ok-val  ok?  ok-val)\n  (err-val err? err-msg))\n\n;; Using match/strict signals that this should cover all variants.\n;; If you accidentally omit a branch and the missing case is hit at runtime,\n;; match raises an error:\n(define (unwrap r)\n  (match/strict result r\n    [(ok-val v) v]))\n\n;; This works fine when called with ok-val, but raises an error on err-val:\n;; (unwrap (make-err \"oops\"))\n;; => Error: match: no matching clause ...\n\n;; A complete version:\n(define (result->string r)\n  (match/strict result r\n    [(ok-val  v) (format \"ok: ~a\" v)]\n    [(err-val m) (format \"error: ~a\" m)]))\n\n(result->string (make-ok 42))        ; => \"ok: 42\"\n(result->string (make-err \"failed\")) ; => \"error: failed\"\n```\n\n---\n\n## Exhaustiveness Checking with `match/strict`\n\n`match/strict` is a documentation and defensive-programming convention. Its syntax is:\n\n```scheme\n(match/strict hierarchy-name expr clause ...)\n;; or, omitting the hierarchy name:\n(match/strict expr clause ...)\n```\n\nBoth forms compile identically to `match`. The difference is semantic intent: using `match/strict` with a `define-sealed-hierarchy` name signals that you expect all variants to be covered. If a variant is hit at runtime without a matching clause, `match` (and therefore `match/strict`) raises an error rather than silently returning an unspecified value.\n\nThis is distinct from compile-time exhaustiveness checking (which would require dependent types or a more complex macro system). Think of `match/strict` as the equivalent of a programmer assertion: \"I believe this match is complete; if it is not, fail loudly.\"\n\nWorkflow:\n\n1. Define your type hierarchy with `define-sealed-hierarchy`.\n2. Use `match/strict hier-name` for all dispatch over that hierarchy.\n3. When you add a new variant to the hierarchy, grep for `match/strict hier-name` to find all sites that need updating \u2014 and if any are missed, a runtime error with the unmatched value will identify the gap.\n\n---\n\n## Comparison with Standard Match\n\n| Feature | `(std match)` | `(std match2)` |\n|---------|---------------|----------------|\n| Wildcards and variables | Yes | Yes |\n| Literals (number, string, boolean, symbol) | Yes | Yes |\n| `(list ...)`, `(cons ...)`, `(vector ...)` | Yes | Yes |\n| `(? pred)` predicate | Yes | Yes |\n| `(? pred -> var)` predicate with binding | No | Yes |\n| `(=> proc var)` view pattern | No | Yes |\n| `(and ...)`, `(or ...)`, `(not ...)` | No | Yes |\n| `(box pat)` box patterns | No | Yes |\n| `(where guard)` inline guards | No | Yes |\n| Named struct patterns | No | Yes, via `define-match-type` |\n| Sealed hierarchies | No | Yes, via `define-sealed-hierarchy` |\n| Active (user-defined extractor) patterns | No | Yes, via `define-active-pattern` |\n| Strict/exhaustive mode | No | Yes, via `match/strict` |\n\nThe pattern dispatch in `match2` is runtime-based: `(TypeName ...)` patterns check a global hashtable to determine whether `TypeName` is an active pattern or a struct type. This means struct types and active patterns must be registered before any code that uses them runs (registration happens at module load time when using `define-match-type` or `define-active-pattern`).\n"}
 {"text": "<!-- FILE: docs/status.md -->\n# Jerboa Project Status\n\nUpdated 2026-03-21.\n\n## What Jerboa Is\n\nJerboa reimplements Gerbil Scheme's API surface as pure stock Chez Scheme\nlibraries \u2014 364 modules, ~2,900 tests, 13 fuzzing harnesses. No custom runtime,\nno patched Chez. The design prioritizes security, safety, and Claude-friendliness:\n`(jerboa prelude safe)` gives AI-generated code a safe-by-default environment with\ncontracts, structured concurrency, timeouts, and kernel sandboxing out of the box.\n\n---\n\n## Assessment Grades\n\n| Category | Grade | Notes |\n|----------|:---:|-------|\n| Security architecture | A+ | Allowlist sandbox, capabilities, taint, real Landlock + seccomp |\n| Safety (safe prelude) | A | Contracts, finalizers, SQL injection detection |\n| Safety (raw prelude) | B | No contracts, no guardian net, fork-thread exposed |\n| Claude-friendliness | A | Safe prelude is the recommended import |\n| Resource management | A | with-resource + guardian + structured concurrency |\n| Error diagnostics | A- | Full condition hierarchy, with-context, 14 lint rules |\n| Type safety | A- | Runtime + compile-time, fatal mode available |\n| Build integrity | B+ | Content-addressed artifacts + SBOM, no full orchestration |\n| Performance | B+ | Chez engines, Rust native backend, WPO |\n\n---\n\n## Implemented Features\n\n### Security\n\n| Module | Description |\n|--------|-------------|\n| `(std security sandbox)` | One-call sandbox: `run-safe` / `run-safe-eval` with fork-based isolation, Landlock + seccomp + capabilities + engine timeout |\n| `(std security restrict)` | Allowlist-only sandbox \u2014 113 bindings, no blocklist. `read` replaced with depth-limited `jerboa-read` (no `#.` read-eval) |\n| `(std security landlock)` | Kernel Landlock syscalls for filesystem sandboxing via foreign-alloc struct packing |\n| `(std security seccomp)` | BPF bytecode generation + seccomp(2) installation with architecture validation |\n| `(std security capability)` | Capability-based security with sealed opaque records + CSPRNG nonces \u2014 unforgeable by construction |\n| `(std security taint)` | Taint tracking with safe sinks \u2014 prevents untrusted data reaching `system`, `open-output-file`, etc. |\n| `(std security flow)` | Information flow control with label lattice |\n| `(std security privsep)` | Privilege separation via real fork(2) |\n| `(std secure-mem)` | mlock, guard pages, explicit_bzero, DONTDUMP, DONTFORK |\n| Rust native backend | Single `libjerboa_native.so` replaces 10+ C deps (ring for crypto, flate2, regex). `ffi_wrap()`/`set_last_error()` panic handling |\n| 13 fuzzing harnesses | Configurable depth limits across input parsers |\n| AI attack hardening | 13 findings addressed (empty-host = deny, symlink resolution, etc.) |\n\n### Safety\n\n| Module | Description |\n|--------|-------------|\n| `(std safe)` | Contract-checked stdlib: SQLite, TCP, File I/O, JSON with pre/post-condition checks. Runtime SQL injection heuristics. `*safe-mode*` parameter: `'check` or `'release` |\n| `(std resource)` | `with-resource` macro with LIFO cleanup via `dynamic-wind` |\n| `(std safe)` guardian net | Guardian-based finalizer safety net \u2014 warns when handles are GC'd without close, with best-effort cleanup |\n| `(std safe-timeout)` | `with-timeout` using Chez engines. `*default-timeout*` (30s) |\n| `(std safe-fasl)` | Rejects procedures, enforces record type registry, size limits, cycle detection |\n| `(jerboa prelude safe)` | Re-exports all safe APIs under standard names. Excludes `fork-thread` and raw FFI. Safe path is the default |\n\n### Error Handling\n\n| Module | Description |\n|--------|-------------|\n| `(std error conditions)` | Full condition hierarchy: `&jerboa` -> `&jerboa-network`, `&jerboa-db`, `&jerboa-actor`, `&jerboa-resource`, `&jerboa-timeout`, `&jerboa-serialization`, `&jerboa-parse` with subtypes |\n| `(std error context)` | `with-context` for automatic context accumulation in error diagnostics |\n\n### Types\n\n| Module | Description |\n|--------|-------------|\n| `(std typed)` + `(std typed env)` + `(std typed infer)` | Gradual type system with bidirectional inference, subtyping, 100+ builtins |\n| `(std typed check)` | Compile-time type checking via expand-time inference. `*type-errors-fatal*` for strict mode |\n| Refinement, linear, affine, phantom, GADTs, typeclasses, row polymorphism, HKTs | `define/r` and `lambda/r` for checked refinements |\n\n### Concurrency\n\n| Module | Description |\n|--------|-------------|\n| `(std concur structured)` | `with-task-scope`, `scope-spawn`, `task-await`, `task-cancel`, `parallel`, `race` |\n| `(std concur deadlock)` | Runtime deadlock detection via wait-for graph + DFS cycle detection. `make-checked-mutex`, `with-checked-mutex` |\n| `(std concur)` annotations | Thread-safety annotations with eq-hashtable tracking |\n| `(std actor bounded)` | `spawn-bounded-actor` with 10,000 message default capacity. Strategies: `'block`, `'drop`, `'error` |\n\n### Build\n\n| Module | Description |\n|--------|-------------|\n| `(build reproducible)` | SHA-256 content hashing (FNV-1a fallback), safe `mkdir-p`, content-addressed artifact store |\n| `(build sbom)` | Cargo.lock + Cargo.toml parsers for Rust dep detection. `detect-all-deps` aggregates Scheme + C + Rust into SBOM |\n\n### Text Processing \u2014 Regex, Patterns, PEG\n\n| Module | Description |\n|--------|-------------|\n| `(std regex)` | Unified regex facade: `re`, `re-match?`, `re-search`, `re-find-all`, `re-replace`, `re-split`, `re-fold`, named captures via SRE `(=> name ...)`. Auto-selects Rust native or pregexp backend |\n| `(std rx)` | Composable pattern macros: `rx` compiles SRE inline, `define-rx` creates named patterns that can be spliced into later definitions. SRE keyword shadowing prevented |\n| `(std rx patterns)` | 30+ pre-built patterns: IPv4, email, UUID, semver, ISO 8601 date/time, identifiers, hex colors, numbers, quoted strings, URLs, JWT, MAC addresses |\n| `(std peg)` | PEG grammar system with packrat memoization: `define-grammar` macro, named captures, `drop`, `sep-by`, complement, mutual recursion. O(n) per rule |\n| `(std srfi srfi-115)` | SRFI-115 SRE compiler extended with `(embed ...)` for pattern splicing, `sre->named-groups` for capture tracking, char-class union via `(or ...)` inside `(~ ...)` |\n| `#r\"...\"` raw strings | Jerboa reader syntax: backslashes are literal, no escape processing. Eliminates regex escape hell |\n\n### Linting\n\n| Module | Description |\n|--------|-------------|\n| `(std lint)` | 14 rules: `unused-define`, `shadowed-define`, `redefine-builtin`, `unsafe-import`, `duplicate-import`, `unused-only-import`, `bare-error`, `sql-interpolation`, `empty-begin`, `single-arm-cond`, `missing-else`, `deep-nesting`, `long-lambda`, `magic-number` |\n\n### Decompression Bomb Protection\n\n| Input | Limit |\n|-------|-------|\n| JSON | `*json-max-depth*` (512), `*json-max-string-length*` (10 MB) |\n| XML | `*sxml-max-depth*` (512), `*sxml-max-output-size*` (50 MB) |\n| FASL | `*fasl-max-object-count*` (1M), `*fasl-max-byte-size*` (100 MB) |\n| Zlib | 100 MB decompression limit in Rust native backend |\n\n---\n\n## Known Limitations\n\n| Module | Limitation | Reason |\n|--------|-----------|--------|\n| Landlock | x86_64 only, requires Linux 5.13+ | Syscall numbers are architecture-specific |\n| Seccomp | x86_64 only, filters are irreversible | BPF bytecode is architecture-specific by design |\n| typed/check | Compile-time checks are warnings by default | Gradual typing standard practice; use `*type-errors-fatal*` for strict |\n| build/reproducible | Falls back to FNV-1a if `(std crypto hash)` unavailable | SHA-256 requires the crypto module |\n| concur/deadlock | Opt-in via `make-checked-mutex` | Auto-instrumenting all mutexes would break existing code |\n| Lint | No full unbound-identifier detection or call-site arity checking | Requires compile-time analysis beyond static linting |\n| Build | No bit-identical reproducible build verification end-to-end | Full build orchestration pipeline not implemented |\n\n---\n\n## Remaining Work\n\n1. **Race detector** \u2014 No dynamic race detection. Structured concurrency reduces risk but does not eliminate it for code using raw `fork-thread` via `(jerboa prelude)`.\n2. **Full build orchestration pipeline** \u2014 Content-addressed artifacts and SBOM exist, but end-to-end reproducible build orchestration is not wired up.\n\n---\n\n## Stub/Partial Audit Result\n\nOriginal audit found 2 stubs and 6 partials. **All are now fixed (0 stubs remaining).**\n\n| Module | Was | Resolution |\n|--------|-----|------------|\n| security/landlock | STUB (no kernel calls) | Real foreign-alloc struct packing + real syscalls |\n| security/seccomp | STUB (no BPF generation) | Real BPF bytecode generator + seccomp(2) install |\n| typed/check | PARTIAL (phase error, warnings only) | Fixed phase imports, added `*type-errors-fatal*` |\n| concur/deadlock | PARTIAL (no drop-in replacements) | Added `make-checked-mutex`, `with-checked-mutex` |\n| build/reproducible | PARTIAL (shell injection, weak hash) | Safe `mkdir-p`, SHA-256 with fallback |\n| build/sbom | PARTIAL (no Rust deps) | Cargo.lock + Cargo.toml parsers |\n| concur | PARTIAL (no distributed) | Was always real for local use; documented as limitation |\n\n---\n\n## Key Differentiators vs. Other Languages for Claude\n\n1. Allowlist sandbox \u2014 best-in-class for running AI-generated code safely\n2. Capability-based security \u2014 unforgeable by construction\n3. Chez engines \u2014 preemptive timeout without OS signals\n4. Taint tracking \u2014 prevents injection from untrusted sources\n5. Rust native backend \u2014 memory-safe FFI without C footguns\n6. Safe-by-default prelude \u2014 Claude gets safety without asking for it\n7. Real Landlock + seccomp \u2014 kernel-enforced sandboxing (Linux 5.13+)\n"}
-{"text": "<!-- FILE: docs/quickstart.md -->\n# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs\n"}
+{"text": "<!-- FILE: docs/quickstart.md -->\n# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs\n"}
 {"text": "<!-- FILE: docs/async.md -->\n# `(std async)` \u2014 Async I/O Runtime\n\n## Overview\n\n`(std async)` provides a structured-concurrency runtime for Jerboa (a Chez Scheme dialect) built on top of the algebraic effects system in `(std effect)`. It gives you cooperative, task-based async programming without abandoning native threads \u2014 instead it uses threads as the underlying suspension mechanism while exposing a clean effect-based API.\n\n### Thread-per-task model\n\nEach async task runs in its own OS thread. Suspension is real thread blocking:\n\n- `(Async await promise)` \u2014 blocks the current thread via a mutex/condition variable until the promise resolves\n- `(Async spawn thunk)` \u2014 forks a new OS thread with Async handlers pre-installed, then returns immediately to the caller\n- `(Async sleep ms)` \u2014 calls Chez Scheme's `sleep` on the current thread\n\nThis is distinct from a cooperative event loop (like Node.js or Tokio): there is no central scheduler and no `poll`-based reactor. Concurrency comes from OS-level thread scheduling. The benefit is that truly blocking C calls (filesystem, sockets via FFI) work without any special adaptation.\n\n### Built on algebraic effects\n\nThe entire suspension protocol is expressed as the `Async` algebraic effect (defined with `defeffect` from `(std effect)`). Effect handlers installed by `run-async` intercept each `Async` operation and implement it with mutex/condition-variable primitives from Chez Scheme's built-in thread library. You can write your own handlers on top of `Async::descriptor` to intercept, log, or mock async operations in tests.\n\n### Dependencies\n\n```\n(std async) imports:\n  (chezscheme)       \u2014 fork-thread, mutex, condition, sleep\n  (std effect)       \u2014 defeffect, with-handler, resume\n  (std misc channel) \u2014 channel-get, channel-put, channel-try-get\n```\n\n---\n\n## Core API\n\n### Effect descriptor\n\n```scheme\nAsync::descriptor\n```\n\nThe unique `effect-descriptor` record identifying the `Async` effect. You rarely reference this directly, but it is exported so that external code can install custom handlers with `with-handler` using the `Async` name, or introspect the handler stack via `*effect-handlers*`.\n\n---\n\n### `Async` \u2014 effect operations\n\n`Async` is a macro generated by `defeffect`. It has three operations:\n\n#### `(Async await promise)`\n\nSuspend the current task until `promise` is resolved, then return its value. Internally calls `promise-wait!` which performs `condition-wait` in a loop \u2014 a true blocking wait on the OS thread.\n\nReturns the value passed to `async-promise-resolve!`.\n\n```scheme\n(run-async\n  (lambda ()\n    (let ([p (make-async-promise)])\n      ;; Resolve the promise from a spawned task\n      (Async spawn (lambda () (async-promise-resolve! p 42)))\n      ;; Suspend until resolved\n      (Async await p)))   ;=> 42\n```\n\n#### `(Async spawn thunk)`\n\nLaunch `thunk` as a concurrent task in a new OS thread. Returns `(void)` immediately; the caller is not blocked. The new thread runs with the same Async handler infrastructure installed.\n\n```scheme\n(Async spawn (lambda ()\n               (display \"running concurrently\\n\")))\n```\n\n#### `(Async sleep ms)`\n\nSleep the current task thread for `ms` milliseconds. Uses Chez Scheme's `sleep` with a `time-duration` record constructed from the millisecond count.\n\n```scheme\n(Async sleep 500)   ; sleep 500 ms\n```\n\n---\n\n### `run-async`\n\n```scheme\n(run-async thunk) => value\n```\n\nEntry point for async code. Runs `thunk` in a new thread with Async effect handlers installed, blocks the calling thread until the thunk completes, and returns the thunk's result.\n\n- Installs the three Async handlers (`await`, `spawn`, `sleep`) via `install-async-handlers!`.\n- Wraps the thunk in a `guard` that catches any exception, prints it to `current-error-port`, and re-raises it so the main thread sees the failure (the promise is resolved with `raise-continuable`).\n- The main thread blocks on `promise-wait!` \u2014 a `condition-wait` loop \u2014 until the root task finishes.\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n(run-async\n  (lambda ()\n    (display \"hello from async\\n\")\n    42))\n;=> 42\n```\n\n---\n\n### `run-async/workers`\n\n```scheme\n(run-async/workers thunk n-workers) => value\n```\n\nIdentical to `run-async` in behavior. The `n-workers` argument is accepted for API compatibility but has no effect: since each `(Async spawn ...)` call creates a dedicated OS thread, there is no fixed worker pool to size. Future versions may use `n-workers` to cap parallelism via a semaphore.\n\n```scheme\n(run-async/workers my-thunk 4)   ; n-workers is currently ignored\n```\n\n---\n\n### Promises\n\nPromises are the fundamental coordination primitive. A promise is a write-once, broadcast-notify cell backed by a mutex and condition variable.\n\n#### `(make-async-promise)` => promise\n\nCreate a new, unresolved promise.\n\n```scheme\n(define p (make-async-promise))\n```\n\n#### `(async-promise? x)` => boolean\n\nReturn `#t` if `x` is an async promise.\n\n#### `(async-promise-resolve! p val)`\n\nResolve promise `p` with value `val`. Thread-safe. If `p` is already resolved, the call is a no-op (the first write wins). All threads waiting on `p` via `(Async await p)` are unblocked via `condition-broadcast`.\n\n```scheme\n(async-promise-resolve! p 'done)\n```\n\n#### `(async-promise-resolved? p)` => boolean\n\nReturn `#t` if the promise has been resolved. This is a non-blocking poll; it does not synchronize with the condition variable beyond the mutex acquisition implied by the record field read.\n\n#### `(async-promise-value p)` => value\n\nReturn the resolved value. Behavior is unspecified if called before the promise is resolved; use `(async-promise-resolved? p)` or `(Async await p)` to ensure resolution first.\n\n---\n\n### Task management\n\n#### `(async-task thunk)` => promise\n\nSpawn a concurrent task that runs `thunk` and captures its return value in a new promise. Returns the promise immediately. The task is launched via `(Async spawn ...)`, so this must be called inside an `run-async` context.\n\n```scheme\n(run-async\n  (lambda ()\n    (let ([p (async-task (lambda () (expensive-computation)))])\n      ;; ... do other work ...\n      (Async await p))))  ; block until computation finishes\n```\n\n#### `(async-task? x)` => boolean\n\nAlias for `async-promise?`. An `async-task` handle is just an `async-promise`.\n\n---\n\n### Async channels\n\nAsync channels bridge `(std misc channel)` (which uses OS-level blocking internally) with the `Async` effect so that waiting for channel data suspends via `(Async await ...)` rather than blocking the thread with a raw `channel-get`.\n\n#### `(async-channel-get ch)` => value\n\nGet the next value from channel `ch`. If the channel already has data, it is returned immediately via `channel-try-get`. If the channel is empty, a helper thread is spawned that blocks on `channel-get`, and the current task suspends via `(Async await promise)` until the helper thread delivers a value.\n\n#### `(async-channel-put ch val)`\n\nPut `val` into channel `ch`. Always asynchronous: a helper thread is forked to call `channel-put` (which may block if `ch` is bounded and full), and the current task suspends via `(Async await promise)` until the put completes.\n\n> **Note:** Both channel operations spawn a helper OS thread per call. For high-throughput channels, prefer using `channel-get`/`channel-put` directly from dedicated producer/consumer threads rather than through the Async channel wrappers.\n\n---\n\n### `async-sleep`\n\n```scheme\n(async-sleep ms)\n```\n\nSleep the current async task for `ms` milliseconds. Thin wrapper around `(Async sleep ms)`. Must be called inside a `run-async` context.\n\n```scheme\n(run-async\n  (lambda ()\n    (display \"before\\n\")\n    (async-sleep 1000)\n    (display \"after 1 second\\n\")))\n```\n\n---\n\n## Working examples\n\n### 1. Basic async task with promise\n\nSpawn a computation, await its result.\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n(run-async\n  (lambda ()\n    (let ([p (async-task (lambda ()\n                           ;; Simulate work\n                           (async-sleep 100)\n                           (* 6 7)))])\n      (let ([result (Async await p)])\n        (printf \"Result: ~a~%\" result)))))\n; prints: Result: 42\n```\n\n### 2. Parallel tasks with join\n\nRun two independent computations in parallel and join their results.\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n(define (parallel-add a b)\n  (run-async\n    (lambda ()\n      (let ([pa (async-task (lambda () (begin (async-sleep 50) a))))\n            [pb (async-task (lambda () (begin (async-sleep 80) b)))])\n        ;; Both tasks run concurrently; total wall time ~80 ms, not 130 ms\n        (+ (Async await pa) (Async await pb))))))\n\n(printf \"~a~%\" (parallel-add 10 32))   ; => 42\n```\n\n### 3. Producer/consumer with async channels\n\n```scheme\n(import (chezscheme) (std effect) (std async) (std misc channel))\n\n(run-async\n  (lambda ()\n    (let ([ch (make-channel 4)])   ; bounded channel, capacity 4\n\n      ;; Producer: send 5 items then a sentinel\n      (Async spawn\n        (lambda ()\n          (let loop ([i 0])\n            (when (< i 5)\n              (async-channel-put ch i)\n              (async-sleep 10)\n              (loop (+ i 1))))\n          (async-channel-put ch 'done)))\n\n      ;; Consumer: drain until sentinel\n      (let consume ()\n        (let ([msg (async-channel-get ch)])\n          (unless (eq? msg 'done)\n            (printf \"got: ~a~%\" msg)\n            (consume))))\n\n      (display \"consumer finished\\n\"))))\n```\n\n### 4. HTTP-like request with timeout\n\nPattern for racing an async operation against a deadline.\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n(define (with-timeout ms thunk)\n  ;; Returns (values result #t) on success or (values #f #f) on timeout\n  (run-async\n    (lambda ()\n      (let ([result-p  (async-task thunk)]\n            [timeout-p (make-async-promise)])\n        ;; Timeout task\n        (Async spawn\n          (lambda ()\n            (async-sleep ms)\n            (async-promise-resolve! timeout-p 'timeout)))\n        ;; Race: whichever promise resolves first wins\n        ;; We poll both \u2014 a proper select would use channel-select\n        (let poll ()\n          (cond\n            [(async-promise-resolved? result-p)\n             (values (async-promise-value result-p) #t)]\n            [(async-promise-resolved? timeout-p)\n             (values #f #f)]\n            [else\n             (async-sleep 5)\n             (poll)]))))))\n\n(define-values (val ok)\n  (with-timeout 200\n    (lambda ()\n      (async-sleep 100)   ; fast enough\n      \"response body\")))\n\n(if ok\n  (printf \"Got: ~a~%\" val)\n  (printf \"Timed out~%\"))\n```\n\n### 5. Fan-out / gather pattern\n\nDispatch N tasks in parallel and collect all results.\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n(define (gather thunks)\n  ;; Returns a list of results in the same order as thunks\n  (run-async\n    (lambda ()\n      (let ([promises (map async-task thunks)])\n        (map (lambda (p) (Async await p)) promises)))))\n\n(define results\n  (gather\n    (list\n      (lambda () (begin (async-sleep 30) 'a))\n      (lambda () (begin (async-sleep 10) 'b))\n      (lambda () (begin (async-sleep 20) 'c)))))\n\n(display results)   ; => (a b c)\n; Total wall time ~30 ms, not 60 ms\n```\n\n---\n\n## Error handling\n\nWhen a task spawned by `async-task` or `(Async spawn ...)` throws an unhandled exception:\n\n- **`async-task`**: the exception propagates inside the spawned thread. Because `async-task` does not wrap the thunk in a `guard`, the exception will be printed by Chez Scheme's default thread-error handler and the task's promise will **never resolve**. Any `(Async await p)` on that promise will block forever.\n\n  Defensive pattern \u2014 wrap the thunk yourself:\n\n  ```scheme\n  (define (safe-task thunk)\n    (let ([p (make-async-promise)])\n      (Async spawn\n        (lambda ()\n          (guard (exn [#t (async-promise-resolve! p (cons 'error exn))])\n            (async-promise-resolve! p (cons 'ok (thunk))))))\n      p))\n  ```\n\n- **`run-async` root task**: the `guard` in `run-async` catches any exception thrown by the root thunk, prints it to `current-error-port`, and resolves the result promise with `raise-continuable`. This means the exception is re-raised in the calling thread (the thread that called `run-async`), so the caller sees a normal Chez Scheme condition.\n\n  ```scheme\n  (guard (exn [#t (printf \"caught: ~a~%\" (condition-message exn))])\n    (run-async (lambda () (error \"boom\" \"test\"))))\n  ; stderr: run-async error: boom\n  ; caught: boom\n  ```\n\n---\n\n## Performance\n\n| Scenario | Recommendation |\n|---|---|\n| CPU-bound parallel work | Use `async-task` / `gather` \u2014 each gets a real OS thread |\n| Many short-lived tasks | Overhead per task is `fork-thread` cost; avoid spawning thousands |\n| Blocking FFI/IO | Works naturally \u2014 threads block independently |\n| High-throughput channels | Use `channel-get`/`channel-put` directly from dedicated threads; avoid `async-channel-get`/`async-channel-put` (each call spawns a helper thread) |\n| Sequential async code | `run-async` with `async-sleep` / `Async await` is fine; no event-loop overhead |\n\n`run-async/workers n` currently does not limit parallelism. If you need to cap OS thread count (e.g., for a bounded thread pool), implement a semaphore-based wrapper around `async-task`.\n\n---\n\n## Integration with `(std effect)`\n\n`Async` is defined with `defeffect`, which generates:\n\n1. `Async::descriptor` \u2014 a unique `effect-descriptor` record used as the key in the handler hashtable.\n2. A macro `Async` that expands `(Async op arg ...)` into `(effect-perform Async::descriptor 'op (list arg ...))`.\n\n`effect-perform` captures the current one-shot continuation (`call/1cc`) and calls the matching handler procedure with `(k . args)`. The handler can then:\n\n- Call `(resume k val)` to continue the task with `val` as the result of the `Async` expression.\n- Block the current thread before resuming (as the `await` and `sleep` handlers do).\n- Fork a new thread and resume immediately (as the `spawn` handler does).\n\nThe handler stack is stored in a thread-local parameter `*effect-handlers*` (a list of `eq-hashtable` frames). Each `with-handler` call pushes a new frame; `run-with-handler` installs it via `parameterize`. Because each spawned task runs in its own thread, `fork-thread` inherits the parent's thread-parameter values, so child tasks start with the same handler stack depth \u2014 but `install-async-handlers!` then installs a fresh set of Async handlers on top for the new thread.\n\n### Replacing handlers for testing\n\nYou can intercept `Async` operations in unit tests by wrapping code in `with-handler` before it reaches the default handlers:\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n;; Mock: record all sleep durations, never actually sleep\n(define recorded-sleeps '())\n\n(with-handler\n  ([Async\n    (sleep (k ms)\n      (set! recorded-sleeps (cons ms recorded-sleeps))\n      (resume k (void)))])   ; override: skip the real sleep\n  (run-async\n    (lambda ()\n      (async-sleep 100)\n      (async-sleep 200))))\n\n(display recorded-sleeps)   ; => (200 100)\n```\n\nNote that `with-handler` pushes a frame on top of the stack, so the inner `with-handler` in the example above takes precedence over the Async handlers installed by `run-async`, because `effect-perform` searches from the innermost frame outward.\n\n---\n\n## Summary of exported symbols\n\n| Symbol | Kind | Purpose |\n|---|---|---|\n| `Async` | macro | Perform `await`, `spawn`, or `sleep` operations |\n| `Async::descriptor` | value | Effect descriptor for the `Async` effect |\n| `run-async` | procedure | Run a thunk in async context, block until done |\n| `run-async/workers` | procedure | Like `run-async`; `n-workers` arg is a no-op |\n| `make-async-promise` | procedure | Create an unresolved promise |\n| `async-promise?` | procedure | Type predicate |\n| `async-promise-resolve!` | procedure | Resolve a promise (write-once, thread-safe) |\n| `async-promise-resolved?` | procedure | Non-blocking poll for resolution |\n| `async-promise-value` | procedure | Read the resolved value |\n| `async-task` | procedure | Spawn a task, return its result promise |\n| `async-task?` | procedure | Alias for `async-promise?` |\n| `async-channel-get` | procedure | Get from channel, suspending via `Async await` |\n| `async-channel-put` | procedure | Put to channel, suspending via `Async await` |\n| `async-sleep` | procedure | Sleep current task for N milliseconds |\n"}
 {"text": "<!-- FILE: docs/native-rust.md -->\n# Replacing C Dependencies with a Unified Rust Native Library\n\nJerboa's C library dependencies are being replaced with a single Rust shared library (`libjerboa_native.so`). Rust implementations are complete for crypto, compression, regex, databases, and OS integration. TLS (rustls) and LevelDB are still pending. The legacy chez-* C wrappers remain available as fallbacks. Every Rust module is callable from Chez Scheme via the same `foreign-procedure` FFI.\n\n---\n\n## Table of Contents\n\n1. [Motivation](#motivation)\n2. [Current C Dependencies](#current-c-dependencies)\n3. [Rust Replacements](#rust-replacements)\n4. [Architecture](#architecture)\n5. [C ABI Design](#c-abi-design)\n6. [FFI Binding Patterns](#ffi-binding-patterns)\n7. [Secure Memory Region](#secure-memory-region)\n8. [What Each Swap Fixes](#what-each-swap-fixes)\n9. [Build Integration](#build-integration)\n10. [Migration Strategy](#migration-strategy)\n11. [Implementation Roadmap](#implementation-roadmap)\n\n---\n\n## Motivation\n\nJerboa currently links 9+ C shared libraries for crypto, TLS, compression, regex, databases, and OS integration. (YAML was previously a C dependency but is now pure Scheme.) Each C library is an independent attack surface: its own CVE history, its own build system, its own ABI compatibility concerns.\n\nReplacing them with a single Rust shared library (`libjerboa_native.so`) provides:\n\n1. **Memory safety** across all native code \u2014 buffer overflows, use-after-free, and double-free become compile-time errors\n2. **A single dependency** instead of 10+ \u2014 one build, one audit surface, one update path\n3. **Solved vulnerability classes** \u2014 ReDoS eliminated (Rust regex uses NFA, not backtracking), timing-safe crypto by default (ring). (YAML parsing is now pure Scheme \u2014 no C/Rust needed.)\n4. **Stable C ABI** \u2014 Rust's `extern \"C\"` produces `.so` files that Chez Scheme loads identically to C libraries\n5. **Reproducible builds** \u2014 Cargo.lock pins every transitive dependency with exact hashes\n\n---\n\n## Current C Dependencies\n\n### Required\n\n| Library          | Module(s)                      | Purpose                                                     |\n|------------------|--------------------------------|-------------------------------------------------------------|\n| **glibc / musl** | `std/os/*`, `std/misc/process` | Core OS: mkstemp, getpid, kill, mmap, dup2, signal handling |\n\n### Crypto and Network\n\n| Library                    | Module(s)                                                     | Purpose                                                                                      |\n|----------------------------|---------------------------------------------------------------|----------------------------------------------------------------------------------------------|\n| ~~**libcrypto.so** (OpenSSL)~~ | ~~`std/crypto/native`~~  | **Eliminated** \u2014 Digests, HMAC, AES-GCM, PBKDF2, CSPRNG now via ring in `libjerboa_native` |\n| ~~**libssl.so** (OpenSSL)~~    | ~~`std/net/tls`~~        | **Eliminated** \u2014 TLS 1.2/1.3 now via rustls in `libjerboa_native`                           |\n\n### Optional Wrappers (chez-* repos)\n\n| Library                    | Shim            | Module              | Purpose                     |\n|----------------------------|-----------------|---------------------|-----------------------------|\n| **libz.so**                | chez-zlib       | `std/compress/zlib` | Deflate/inflate compression |\n| **libpcre2-8.so**          | chez-pcre2      | `std/pcre2`         | Perl-compatible regex       |\n| **libsqlite3.so**          | chez-sqlite     | `std/db/sqlite`     | SQLite database             |\n| **libpq.so**               | chez-postgresql | `std/db/postgresql` | PostgreSQL client           |\n| **libleveldb.so**          | chez-leveldb    | `std/db/leveldb`    | LevelDB key-value store     |\n| ~~**libyaml.so**~~         | ~~chez-yaml~~   | `std/text/yaml`     | ~~YAML parsing~~ \u2014 **Eliminated**: now pure Scheme with roundtrip support |\n| **libcurl.so** (or libssl) | chez-https      | `std/net/request`   | HTTPS client                |\n\n### Custom C Code\n\n| File                        | Module            | Purpose                                     |\n|-----------------------------|-------------------|---------------------------------------------|\n| **support/landlock-shim.c** | `std/os/landlock` | Landlock LSM syscall wrapper                |\n| **support/jerboa-embed.c**  | `jerboa/embed`    | C API for embedding Chez in C/Rust programs |\n\n### Linux-Specific (No External Library)\n\n| Shim              | Module           | Purpose                         |\n|-------------------|------------------|---------------------------------|\n| chez-epoll        | `std/os/epoll`   | Event polling (direct syscalls) |\n| chez-inotify      | `std/os/inotify` | File watching (direct syscalls) |\n| liburing-ffi.so.2 | `std/os/iouring` | io_uring async I/O              |\n\n---\n\n## Rust Replacements\n\n### Crypto: libcrypto \u2192 ring\n\n| Feature               | Current (OpenSSL)                         | Replacement (ring)                             | Notes                                        |\n|-----------------------|-------------------------------------------|------------------------------------------------|----------------------------------------------|\n| Digests               | `EVP_DigestInit/Update/Final`             | `ring::digest`                                 | SHA-1, SHA-256, SHA-384, SHA-512             |\n| HMAC                  | `HMAC()`                                  | `ring::hmac`                                   | All SHA variants                             |\n| AES-GCM               | `EVP_Encrypt/Decrypt` + `EVP_aes_256_gcm` | `ring::aead`                                   | AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305  |\n| CSPRNG                | `RAND_bytes()`                            | `ring::rand::SystemRandom`                     | Uses OS entropy source                       |\n| PBKDF2                | `PKCS5_PBKDF2_HMAC()`                     | `ring::pbkdf2`                                 | Timing-safe verification built in            |\n| Constant-time compare | `CRYPTO_memcmp()`                         | `ring::constant_time::verify_slices_are_equal` | Default behavior, not opt-in                 |\n| MD5                   | `EVP_md5()`                               | **Not in ring** \u2014 use `md-5` crate             | ring deliberately excludes broken algorithms |\n\n**ring** is audited, powers rustls and Cloudflare's infrastructure, and has no `unsafe` in its Rust API. It uses BoringSSL's C/asm for primitives internally (AES-NI, constant-time implementations) but presents a safe Rust interface.\n\n**Alternative**: RustCrypto crates (`sha2`, `aes-gcm`, `hmac`, `pbkdf2`) \u2014 pure Rust, no C/asm. Slightly slower but fully auditable Rust source. Good for environments where \"no C at all\" is the goal.\n\n### TLS: libssl \u2192 rustls-ffi\n\n| Feature                  | Current (OpenSSL)                    | Replacement (rustls)                                | Notes                                                   |\n|--------------------------|--------------------------------------|-----------------------------------------------------|---------------------------------------------------------|\n| TLS 1.2/1.3              | `SSL_CTX_new`, `SSL_connect`, etc.   | `rustls_client_config_builder`, `rustls_connection` | Full protocol support                                   |\n| Certificate verification | OpenSSL's X.509 stack                | `webpki` + `rustls-native-certs`                    | Uses OS trust store                                     |\n| Hostname verification    | Manual or `SSL_set_tlsext_host_name` | Automatic and mandatory                             | Cannot be disabled \u2014 safe by default                    |\n| Cipher suites            | Configurable (including weak ones)   | Only safe suites, no configuration needed           | TLS_AES_256_GCM, TLS_CHACHA20_POLY1305, TLS_AES_128_GCM |\n\n**rustls-ffi** already exists as a C ABI library specifically designed for non-Rust consumers. It provides a stable C header (`rustls.h`) that Chez can call directly. This is not a hypothetical \u2014 curl, Apache httpd, and other C projects already use rustls-ffi in production.\n\nZero memory-safety CVEs in rustls, compared to hundreds in OpenSSL.\n\n### Regex: libpcre2 \u2192 regex crate\n\n| Feature | Current (PCRE2) | Replacement (regex) | Notes |\n|---------|-----------------|---------------------|-------|\n| Pattern matching | Backtracking NFA | Thompson NFA (finite automata) | **Guaranteed linear-time** \u2014 ReDoS impossible |\n| Unicode | Full Unicode support | Full Unicode support | `\\p{Lu}`, `\\w`, etc. |\n| Capture groups | Named and numbered | Named and numbered | Same API surface |\n| JIT compilation | PCRE2 JIT | Lazy DFA compilation | Comparable performance |\n\nThe Rust regex crate **eliminates ReDoS entirely**. Pathological patterns like `(a+)+b` that hang PCRE2/pregexp complete in microseconds because the engine cannot backtrack. This single swap fixes the entire ReDoS section of security2.md for any code path that uses the native regex engine.\n\n### Compression: libz \u2192 flate2\n\n| Feature | Current (zlib) | Replacement (flate2) | Notes |\n|---------|---------------|---------------------|-------|\n| Deflate | `deflate()` / `inflate()` | `flate2::Compress` / `flate2::Decompress` | Compatible output |\n| Gzip | `gz*` functions | `flate2::read::GzDecoder` | Same format |\n\nflate2 uses `miniz_oxide` (pure Rust) by default. No C code, no unsafe. Performance is within 10% of zlib.\n\n**Decompression bomb protection**: Easy to add a size limit in the Rust wrapper:\n\n```rust\n#[no_mangle]\npub extern \"C\" fn jerboa_inflate(\n    input: *const u8, input_len: usize,\n    output: *mut u8, output_max: usize,\n    output_len: *mut usize\n) -> i32 {\n    // output_max caps decompression \u2014 no billion-byte bombs\n}\n```\n\n### ~~YAML: libyaml \u2192 unsafe-libyaml~~ \u2014 SUPERSEDED\n\n**This swap is no longer needed.** `(std text yaml)` is now a pure Scheme implementation with full roundtrip support (preserves comments, key ordering, scalar styles, block/flow collection styles). No C dependency, no FFI \u2014 the entire libyaml attack surface has been eliminated without needing a Rust replacement.\n\nThe pure Scheme implementation provides:\n- Simple API: `yaml-load`, `yaml-dump` (backward-compatible with old chez-yaml interface)\n- Roundtrip API: `yaml-read`, `yaml-write` (returns/consumes AST nodes with comment and style metadata)\n- Node manipulation: `yaml-mapping-ref`, `yaml-mapping-set!`, `yaml-ref`, `yaml-set!`\n- Security: `*yaml-max-input-size*` (1MB) and `*yaml-max-depth*` (512) limits\n\n### Databases: libsqlite3 \u2192 rusqlite, libpq \u2192 rust-postgres\n\n| Current | Replacement | Notes |\n|---------|-------------|-------|\n| libsqlite3 (via chez-sqlite) | **rusqlite** | Bundles SQLite source or links system lib. Parameterized queries by default. |\n| libpq (via chez-postgresql) | **rust-postgres** | Pure Rust PostgreSQL client. TLS via rustls. No C dependency. |\n| libleveldb (via chez-leveldb) | **rusty-leveldb** or **sled** | rusty-leveldb is API-compatible; sled is pure Rust with different (better) API |\n\n### OS Integration\n\n| Current | Replacement | Notes |\n|---------|-------------|-------|\n| landlock-shim.c | **landlock** crate | Clean Rust API for Landlock ABI v1-v4 |\n| liburing-ffi.so.2 | **io-uring** crate | Safe wrapper over kernel interface |\n| chez-epoll shim | Rust `epoll` wrapper or **mio** | mio abstracts over epoll/kqueue/IOCP |\n| chez-inotify shim | **inotify** crate | Safe wrapper |\n\n---\n\n## Architecture\n\n### Single Shared Library\n\nAll Rust code compiles into one shared library:\n\n```\njerboa-native-rs/\n\u251c\u2500\u2500 Cargo.toml\n\u251c\u2500\u2500 cbindgen.toml              # generates jerboa_native.h\n\u251c\u2500\u2500 src/\n\u2502   \u251c\u2500\u2500 lib.rs                 # top-level: panic handler, init\n\u2502   \u251c\u2500\u2500 crypto.rs              # ring: digest, hmac, aead, csprng, pbkdf2\n\u2502   \u251c\u2500\u2500 tls.rs                 # rustls-ffi: connect, accept, read, write\n\u2502   \u251c\u2500\u2500 regex.rs               # regex crate: compile, match, find, replace\n\u2502   \u251c\u2500\u2500 compress.rs            # flate2: deflate, inflate (with size limits)\n\u2502   \u251c\u2500\u2500 sqlite.rs              # rusqlite: open, prepare, bind, step, finalize\n\u2502   \u251c\u2500\u2500 postgres.rs            # rust-postgres: connect, query, execute\n\u2502   \u251c\u2500\u2500 secure_mem.rs          # mlock, guard pages, explicit_bzero, DONTDUMP\n\u2502   \u251c\u2500\u2500 landlock.rs            # landlock crate: create ruleset, add rules, enforce\n\u2502   \u251c\u2500\u2500 iouring.rs             # io-uring crate: queue init, submit, wait\n\u2502   \u251c\u2500\u2500 epoll.rs               # epoll: create, ctl, wait\n\u2502   \u251c\u2500\u2500 inotify.rs             # inotify: init, add_watch, read_events\n\u2502   \u2514\u2500\u2500 panic.rs               # catch_unwind wrapper for all extern \"C\" functions\n\u251c\u2500\u2500 tests/\n\u2502   \u2514\u2500\u2500 ffi_test.rs            # test C ABI from Rust side\n\u2514\u2500\u2500 target/release/\n    \u2514\u2500\u2500 libjerboa_native.so    # single output: ~2-5MB\n```\n\n```toml\n# Cargo.toml\n[package]\nname = \"jerboa-native\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nring = \"0.17\"\nrustls = \"0.23\"\nrustls-pemfile = \"2\"\nwebpki-roots = \"0.26\"\nregex = \"1\"\nflate2 = \"1\"\n# unsafe-libyaml \u2014 no longer needed: (std text yaml) is pure Scheme\nrusqlite = { version = \"0.32\", features = [\"bundled\"] }\npostgres = \"0.19\"\nlandlock = \"0.4\"\nio-uring = \"0.7\"\ninotify = \"0.11\"\nlibc = \"0.2\"\n```\n\n### Why One Library, Not Many\n\n- **One `load-shared-object` call** in Jerboa \u2014 no hunting for 10 different `.so` files\n- **One build step** \u2014 `cargo build --release` produces everything\n- **One audit surface** \u2014 `cargo audit` checks all transitive dependencies\n- **One version to track** \u2014 Jerboa's Cargo.lock pins the entire native dependency tree\n- **Smaller total size** \u2014 shared Rust runtime, LTO across all modules, dead code elimination\n\n---\n\n## C ABI Design\n\n### Principles\n\n1. **Every function is `extern \"C\"` with `#[no_mangle]`** \u2014 visible to Chez's `foreign-procedure`\n2. **Every function catches panics** \u2014 Rust panics across FFI are undefined behavior\n3. **Opaque handles for stateful objects** \u2014 TLS connections, database connections, regex patterns are opaque pointers\n4. **Error codes, not exceptions** \u2014 return `i32` status codes; error details via `jerboa_last_error()`\n5. **Caller-allocated buffers where possible** \u2014 avoid Rust allocating memory that Scheme must free\n6. **Consistent naming** \u2014 `jerboa_<module>_<operation>` prefix for all functions\n\n### Panic Safety\n\nEvery exported function wraps its body in `catch_unwind`:\n\n```rust\n// src/panic.rs\nuse std::panic;\n\n/// Thread-local error message for the last failed operation\nthread_local! {\n    static LAST_ERROR: std::cell::RefCell<String> = std::cell::RefCell::new(String::new());\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_last_error(buf: *mut u8, buf_len: usize) -> usize {\n    LAST_ERROR.with(|e| {\n        let msg = e.borrow();\n        let bytes = msg.as_bytes();\n        let copy_len = bytes.len().min(buf_len.saturating_sub(1));\n        if !buf.is_null() && copy_len > 0 {\n            unsafe {\n                std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, copy_len);\n                *buf.add(copy_len) = 0; // null terminate\n            }\n        }\n        bytes.len()\n    })\n}\n\npub fn ffi_wrap<F: FnOnce() -> i32 + panic::UnwindSafe>(f: F) -> i32 {\n    match panic::catch_unwind(f) {\n        Ok(code) => code,\n        Err(e) => {\n            let msg = if let Some(s) = e.downcast_ref::<&str>() {\n                s.to_string()\n            } else if let Some(s) = e.downcast_ref::<String>() {\n                s.clone()\n            } else {\n                \"unknown panic\".to_string()\n            };\n            LAST_ERROR.with(|cell| *cell.borrow_mut() = msg);\n            -1 // error sentinel\n        }\n    }\n}\n```\n\n### Example: Crypto Module\n\n```rust\n// src/crypto.rs\nuse ring::{digest, hmac, rand, aead, constant_time};\nuse crate::panic::ffi_wrap;\n\n// --- Digest ---\n\n#[no_mangle]\npub extern \"C\" fn jerboa_sha256(\n    input: *const u8, input_len: usize,\n    output: *mut u8, output_len: usize,\n) -> i32 {\n    ffi_wrap(|| {\n        if input.is_null() || output.is_null() || output_len < 32 {\n            return -1;\n        }\n        let data = unsafe { std::slice::from_raw_parts(input, input_len) };\n        let hash = digest::digest(&digest::SHA256, data);\n        unsafe {\n            std::ptr::copy_nonoverlapping(hash.as_ref().as_ptr(), output, 32);\n        }\n        0 // success\n    })\n}\n\n// --- CSPRNG ---\n\n#[no_mangle]\npub extern \"C\" fn jerboa_random_bytes(output: *mut u8, len: usize) -> i32 {\n    ffi_wrap(|| {\n        if output.is_null() {\n            return -1;\n        }\n        let rng = rand::SystemRandom::new();\n        let buf = unsafe { std::slice::from_raw_parts_mut(output, len) };\n        rand::SecureRandom::fill(&rng, buf).map(|_| 0).unwrap_or(-1)\n    })\n}\n\n// --- Constant-time comparison ---\n\n#[no_mangle]\npub extern \"C\" fn jerboa_timing_safe_equal(\n    a: *const u8, a_len: usize,\n    b: *const u8, b_len: usize,\n) -> i32 {\n    ffi_wrap(|| {\n        if a_len != b_len { return 0; }\n        let sa = unsafe { std::slice::from_raw_parts(a, a_len) };\n        let sb = unsafe { std::slice::from_raw_parts(b, b_len) };\n        if constant_time::verify_slices_are_equal(sa, sb).is_ok() { 1 } else { 0 }\n    })\n}\n\n// --- AEAD (AES-256-GCM) ---\n\n#[no_mangle]\npub extern \"C\" fn jerboa_aead_seal(\n    key: *const u8, key_len: usize,        // 32 bytes for AES-256-GCM\n    nonce: *const u8, nonce_len: usize,     // 12 bytes\n    plaintext: *const u8, pt_len: usize,\n    aad: *const u8, aad_len: usize,\n    output: *mut u8, output_max: usize,     // must be >= pt_len + 16 (tag)\n    output_len: *mut usize,\n) -> i32 {\n    ffi_wrap(|| {\n        // ... validate inputs, construct SealingKey, seal in place ...\n        0\n    })\n}\n```\n\n### Example: Regex Module\n\n```rust\n// src/regex.rs\nuse regex::Regex;\nuse crate::panic::ffi_wrap;\nuse std::collections::HashMap;\nuse std::sync::Mutex;\n\n// Opaque handle system\nstatic REGEX_STORE: Mutex<HashMap<u64, Regex>> = Mutex::new(HashMap::new());\nstatic NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);\n\n#[no_mangle]\npub extern \"C\" fn jerboa_regex_compile(\n    pattern: *const u8, pattern_len: usize,\n    handle: *mut u64,\n) -> i32 {\n    ffi_wrap(|| {\n        let pat = unsafe { std::str::from_utf8_unchecked(\n            std::slice::from_raw_parts(pattern, pattern_len)) };\n        match Regex::new(pat) {\n            Ok(re) => {\n                let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst);\n                REGEX_STORE.lock().unwrap().insert(id, re);\n                unsafe { *handle = id; }\n                0\n            }\n            Err(_) => -1,\n        }\n    })\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_regex_is_match(\n    handle: u64,\n    text: *const u8, text_len: usize,\n) -> i32 {\n    ffi_wrap(|| {\n        let store = REGEX_STORE.lock().unwrap();\n        let re = match store.get(&handle) {\n            Some(r) => r,\n            None => return -1,\n        };\n        let s = unsafe { std::str::from_utf8_unchecked(\n            std::slice::from_raw_parts(text, text_len)) };\n        if re.is_match(s) { 1 } else { 0 }\n    })\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_regex_free(handle: u64) -> i32 {\n    ffi_wrap(|| {\n        REGEX_STORE.lock().unwrap().remove(&handle);\n        0\n    })\n}\n```\n\n---\n\n## FFI Binding Patterns\n\n### Jerboa Side\n\n```scheme\n;; lib/std/native.sls \u2014 load the unified Rust library\n(library (std native)\n  (export load-jerboa-native)\n  (import (chezscheme))\n\n  (define (load-jerboa-native)\n    (load-shared-object \"libjerboa_native.so\")))\n```\n\n```scheme\n;; lib/std/crypto/native.sls \u2014 crypto bindings\n(library (std crypto native)\n  (export sha256 random-bytes timing-safe-equal? aead-seal aead-open)\n  (import (chezscheme) (std native))\n\n  (load-jerboa-native)\n\n  (define jerboa-sha256\n    (foreign-procedure \"jerboa_sha256\"\n      (u8* size_t u8* size_t) int))\n\n  (define (sha256 bv)\n    (let ([out (make-bytevector 32)])\n      (let ([rc (jerboa-sha256 bv (bytevector-length bv) out 32)])\n        (when (< rc 0)\n          (error 'sha256 \"hash failed\"))\n        out)))\n\n  (define jerboa-random-bytes\n    (foreign-procedure \"jerboa_random_bytes\" (u8* size_t) int))\n\n  (define (random-bytes n)\n    (let ([bv (make-bytevector n)])\n      (let ([rc (jerboa-random-bytes bv n)])\n        (when (< rc 0)\n          (error 'random-bytes \"CSPRNG failed\"))\n        bv)))\n\n  (define jerboa-timing-safe-equal\n    (foreign-procedure \"jerboa_timing_safe_equal\"\n      (u8* size_t u8* size_t) int))\n\n  (define (timing-safe-equal? a b)\n    (= 1 (jerboa-timing-safe-equal a (bytevector-length a)\n                                    b (bytevector-length b))))\n\n  ;; ... aead-seal, aead-open similarly\n  )\n```\n\n```scheme\n;; lib/std/pcre2.sls replacement \u2014 now backed by Rust regex\n(library (std regex-native)\n  (export regex-compile regex-match? regex-find regex-free)\n  (import (chezscheme) (std native))\n\n  (load-jerboa-native)\n\n  (define jerboa-regex-compile\n    (foreign-procedure \"jerboa_regex_compile\" (u8* size_t void*) int))\n\n  (define jerboa-regex-is-match\n    (foreign-procedure \"jerboa_regex_is_match\" (unsigned-64 u8* size_t) int))\n\n  (define jerboa-regex-free\n    (foreign-procedure \"jerboa_regex_free\" (unsigned-64) int))\n\n  (define (regex-compile pattern)\n    (let ([bv (string->utf8 pattern)]\n          [handle-box (make-bytevector 8)])\n      (let ([rc (jerboa-regex-compile bv (bytevector-length bv) handle-box)])\n        (when (< rc 0)\n          (error 'regex-compile \"invalid pattern\" pattern))\n        (bytevector-u64-native-ref handle-box 0))))\n\n  (define (regex-match? handle text)\n    (let ([bv (string->utf8 text)])\n      (= 1 (jerboa-regex-is-match handle bv (bytevector-length bv)))))\n\n  (define (regex-free handle)\n    (jerboa-regex-free handle))\n  )\n```\n\n---\n\n## Secure Memory Region\n\nThe secure region allocator from `vs-rust.md`, implemented in Rust:\n\n```rust\n// src/secure_mem.rs\nuse libc::{mmap, munmap, mlock, munlock, madvise, mprotect};\nuse libc::{MAP_PRIVATE, MAP_ANONYMOUS, PROT_READ, PROT_WRITE, PROT_NONE};\nuse libc::{MADV_DONTDUMP, MADV_DONTFORK};\nuse std::ptr;\nuse crate::panic::ffi_wrap;\n\nconst GUARD_PAGE_SIZE: usize = 4096;\n\n#[no_mangle]\npub extern \"C\" fn jerboa_secure_alloc(size: usize) -> *mut u8 {\n    ffi_wrap_ptr(|| {\n        // Allocate: guard page + data + guard page\n        let total = GUARD_PAGE_SIZE + size + GUARD_PAGE_SIZE;\n        let base = unsafe {\n            mmap(ptr::null_mut(), total, PROT_READ | PROT_WRITE,\n                 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)\n        };\n        if base == libc::MAP_FAILED { return ptr::null_mut(); }\n\n        // Protect guard pages (PROT_NONE \u2014 any access = SIGSEGV)\n        unsafe {\n            mprotect(base, GUARD_PAGE_SIZE, PROT_NONE);\n            mprotect(base.add(GUARD_PAGE_SIZE + size), GUARD_PAGE_SIZE, PROT_NONE);\n        }\n\n        let data = unsafe { base.add(GUARD_PAGE_SIZE) as *mut u8 };\n\n        // Lock into RAM \u2014 never swapped to disk\n        unsafe { mlock(data as *mut _, size); }\n\n        // Exclude from core dumps\n        unsafe { madvise(data as *mut _, size, MADV_DONTDUMP); }\n\n        // Don't inherit in child processes\n        unsafe { madvise(data as *mut _, size, MADV_DONTFORK); }\n\n        data\n    })\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_secure_free(ptr: *mut u8, size: usize) -> i32 {\n    ffi_wrap(|| {\n        if ptr.is_null() { return -1; }\n\n        // Wipe \u2014 explicit_bzero is guaranteed not to be optimized away\n        unsafe { libc::explicit_bzero(ptr as *mut _, size); }\n\n        // Unlock\n        unsafe { munlock(ptr as *mut _, size); }\n\n        // Unmap entire region including guard pages\n        let base = unsafe { ptr.sub(GUARD_PAGE_SIZE) };\n        let total = GUARD_PAGE_SIZE + size + GUARD_PAGE_SIZE;\n        unsafe { munmap(base as *mut _, total); }\n\n        0\n    })\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_secure_wipe(ptr: *mut u8, size: usize) -> i32 {\n    ffi_wrap(|| {\n        if ptr.is_null() { return -1; }\n        unsafe { libc::explicit_bzero(ptr as *mut _, size); }\n        0\n    })\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_secure_random_fill(ptr: *mut u8, size: usize) -> i32 {\n    ffi_wrap(|| {\n        if ptr.is_null() { return -1; }\n        let rng = ring::rand::SystemRandom::new();\n        let buf = unsafe { std::slice::from_raw_parts_mut(ptr, size) };\n        ring::rand::SecureRandom::fill(&rng, buf).map(|_| 0).unwrap_or(-1)\n    })\n}\n\n// Helper: ffi_wrap for pointer-returning functions\nfn ffi_wrap_ptr<F: FnOnce() -> *mut u8 + std::panic::UnwindSafe>(f: F) -> *mut u8 {\n    match std::panic::catch_unwind(f) {\n        Ok(ptr) => ptr,\n        Err(_) => ptr::null_mut(),\n    }\n}\n```\n\nJerboa side:\n\n```scheme\n;; lib/std/crypto/secure-mem.sls\n(library (std crypto secure-mem)\n  (export with-secure-region secure-alloc secure-free secure-wipe)\n  (import (chezscheme) (std native))\n\n  (load-jerboa-native)\n\n  (define jerboa-secure-alloc\n    (foreign-procedure \"jerboa_secure_alloc\" (size_t) void*))\n  (define jerboa-secure-free\n    (foreign-procedure \"jerboa_secure_free\" (void* size_t) int))\n  (define jerboa-secure-wipe\n    (foreign-procedure \"jerboa_secure_wipe\" (void* size_t) int))\n  (define jerboa-secure-random-fill\n    (foreign-procedure \"jerboa_secure_random_fill\" (void* size_t) int))\n\n  (define (secure-alloc size)\n    (let ([ptr (jerboa-secure-alloc size)])\n      (when (zero? (ftype-pointer-address ptr))\n        (error 'secure-alloc \"mmap failed\" size))\n      (cons ptr size)))  ;; pair of (pointer . size) for tracking\n\n  (define (secure-free region)\n    (jerboa-secure-free (car region) (cdr region)))\n\n  (define-syntax with-secure-region\n    (syntax-rules ()\n      [(_ ([name size] ...) body ...)\n       (let ([name (secure-alloc size)] ...)\n         (dynamic-wind\n           void\n           (lambda () body ...)\n           (lambda ()\n             (secure-free name) ...)))]))\n  )\n```\n\n---\n\n## What Each Swap Fixes\n\n### Vulnerabilities from security.md\n\n| Vulnerability | Fixed By | How |\n|---------------|----------|-----|\n| **V1**: Command injection in digest | ring (crypto.rs) | No shell, no temp files \u2014 direct hash computation |\n| **V3**: Predictable nonces | ring CSPRNG | `SystemRandom` uses OS entropy |\n| **V5**: Weak actor auth (FNV-1a) | ring HMAC | HMAC-SHA256 with proper key derivation |\n| **V8**: WebSocket handshake stub | ring SHA-1 | Proper `SHA-1(key + GUID)` computation |\n| **C1-C6**: Crypto foundation | ring + secure_mem | Complete crypto suite with secure memory |\n\n### Bug classes from security2.md\n\n| Bug Class | Fixed By | How |\n|-----------|----------|-----|\n| **ReDoS** | Rust regex crate | NFA engine \u2014 linear time guaranteed, backtracking impossible |\n| **Zlib decompression bombs** | flate2 (compress.rs) | Output size limit parameter in Rust wrapper |\n| ~~**YAML C parser bugs**~~ | ~~unsafe-libyaml~~ | **Eliminated** \u2014 `(std text yaml)` is now pure Scheme, no C/Rust needed |\n| **FFI null pointer deref** | Rust wrappers | Every function validates inputs and checks returns |\n| **FFI type confusion** | Rust type system | Rust compiler rejects type mismatches |\n| **Secret material in GC heap** | secure_mem.rs | mlock'd region outside GC, wiped on free |\n\n### What's NOT fixed by Rust swap alone\n\nThese still need the Scheme-side fixes from security2.md:\n\n| Bug Class | Why Rust Doesn't Help |\n|-----------|----------------------|\n| Parser recursion depth (reader, JSON) | Bug is in Scheme code, not C libraries |\n| Bytevector bounds checks (HTTP/2, WebSocket, DNS) | Bug is in Scheme code, not C libraries |\n| Silent data corruption (base64, CSV, hex) | Bug is in Scheme code, not C libraries |\n| Format string injection | Bug is in Scheme code |\n| Sandbox escape vectors | Bug is in Scheme code |\n| Capability forgery (V2) | Bug is in Scheme code |\n\nThe Rust native library and the security2.md Scheme fixes are complementary \u2014 neither is a substitute for the other.\n\n---\n\n## Build Integration\n\n### Makefile Additions\n\n```makefile\n# Rust native library\nRUST_NATIVE_DIR = jerboa-native-rs\nRUST_NATIVE_LIB = $(RUST_NATIVE_DIR)/target/release/libjerboa_native.so\n\n$(RUST_NATIVE_LIB): $(RUST_NATIVE_DIR)/src/*.rs $(RUST_NATIVE_DIR)/Cargo.toml\n\tcd $(RUST_NATIVE_DIR) && cargo build --release\n\nnative: $(RUST_NATIVE_LIB)\n\tcp $(RUST_NATIVE_LIB) lib/\n\nclean-native:\n\tcd $(RUST_NATIVE_DIR) && cargo clean\n\n# Run tests with native library available\ntest-native: native\n\tLD_LIBRARY_PATH=lib $(SCHEME) --libdirs lib --script tests/test-native.ss\n\n# Audit dependencies for known vulnerabilities\naudit-native:\n\tcd $(RUST_NATIVE_DIR) && cargo audit\n\n# Generate C header (for documentation / external consumers)\nheader-native:\n\tcd $(RUST_NATIVE_DIR) && cbindgen --output include/jerboa_native.h\n```\n\n### Static Linking for Single-Binary Deployment\n\nFor `jerboa/build.sls` static binary builds, link the Rust library statically:\n\n```toml\n# Cargo.toml \u2014 also produce a static lib\n[lib]\ncrate-type = [\"cdylib\", \"staticlib\"]\n```\n\n```makefile\n# Static build: link libjerboa_native.a into the Chez binary\nRUST_STATIC_LIB = $(RUST_NATIVE_DIR)/target/release/libjerboa_native.a\n\nstatic-binary: $(RUST_STATIC_LIB)\n\t$(SCHEME) --libdirs lib --script build-static.ss \\\n\t  --extra-libs \"$(RUST_STATIC_LIB) -lpthread -ldl -lm\"\n```\n\nThe result is a single binary with zero runtime dependencies beyond libc \u2014 Chez runtime + Jerboa libraries + Rust native code, all in one ELF file.\n\n---\n\n## Migration Strategy\n\n### Current Status\n\nWe are in **Phase 1** (parallel installation). The Rust implementations are complete for crypto, compression, regex, databases (SQLite, PostgreSQL), and OS integration (epoll, inotify, landlock). However, the \"default\" modules (e.g., `(std db sqlite)`, `(std crypto cipher)`) still import from chez-* C libraries. The Rust-backed modules are available as separate imports (e.g., `(std db sqlite-native)`, `(std crypto native-rust)`).\n\n**TLS is deferred** \u2014 `(std net ssl)` still requires chez-ssl / OpenSSL. The rustls-ffi integration is planned but not yet implemented due to the complexity of stateful TLS session management.\n\n### Phase 1: Parallel Installation (CURRENT)\n\nBoth C libraries and the Rust library are available as separate modules. New code should prefer the `-native` / `native-rust` modules:\n\n```scheme\n;; Preferred \u2014 uses Rust ring via libjerboa_native.so\n(import (std crypto native-rust))\n\n;; Legacy \u2014 uses OpenSSL via chez-crypto\n(import (std crypto cipher))\n```\n\nThis allows incremental testing \u2014 run the full test suite against both backends and compare results.\n\n### Phase 2: Native-First\n\nThe Rust library becomes the default import for all modules that have Rust replacements. The chez-* modules become explicitly legacy.\n\n### Phase 3: C Removal\n\nRemove the chez-* wrapper dependencies entirely. The Rust library is the only native code Jerboa links (besides Chez Scheme itself and libc).\n\n---\n\n## Implementation Roadmap\n\n### Week 1-2: Core \u2014 Crypto + Secure Memory \u2705 IMPLEMENTED\n\n| Task | Crate | Functions | Status |\n|------|-------|-----------|--------|\n| Digests (SHA-1, SHA-256, SHA-384, SHA-512) | ring | `jerboa_sha1`, `jerboa_sha256`, `jerboa_sha384`, `jerboa_sha512` | \u2705 |\n| HMAC | ring | `jerboa_hmac_sha256`, `jerboa_hmac_sha256_verify` | \u2705 |\n| CSPRNG | ring | `jerboa_random_bytes` | \u2705 |\n| Constant-time compare | ring | `jerboa_timing_safe_equal` | \u2705 |\n| AEAD (AES-256-GCM) | ring | `jerboa_aead_seal`, `jerboa_aead_open` | \u2705 |\n| PBKDF2 | ring | `jerboa_pbkdf2_derive`, `jerboa_pbkdf2_verify` | \u2705 |\n| Secure memory region | libc (mmap/mlock) | `jerboa_secure_alloc`, `jerboa_secure_free`, `jerboa_secure_wipe` | \u2705 |\n| Scheme bindings | \u2014 | `(std crypto native-rust)`, `(std crypto secure-mem)` | \u2705 |\n\nRust: `src/crypto.rs`, `src/secure_mem.rs`. Scheme: `lib/std/crypto/native-rust.sls`, `lib/std/crypto/secure-mem.sls`. Tests: `tests/test-native-rust.ss` (31 tests).\n\n### Week 3: TLS \u2014 DEFERRED\n\n| Task | Crate | Functions | Status |\n|------|-------|-----------|--------|\n| TLS client | rustls + rustls-ffi | `jerboa_tls_connect`, `jerboa_tls_read`, `jerboa_tls_write`, `jerboa_tls_close` | \u23f3 |\n| TLS server | rustls + rustls-ffi | `jerboa_tls_accept` | \u23f3 |\n| Certificate loading | rustls-pemfile | `jerboa_tls_load_cert`, `jerboa_tls_load_key` | \u23f3 |\n\nTLS requires stateful session management with complex async I/O. Deferred to a future phase.\n\n### Week 4: Regex + Compression \u2705 IMPLEMENTED\n\n| Task | Crate | Functions | Status |\n|------|-------|-----------|--------|\n| Regex compile/match/find/replace | regex (NFA) | `jerboa_regex_compile`, `jerboa_regex_is_match`, `jerboa_regex_find`, `jerboa_regex_replace_all`, `jerboa_regex_free` | \u2705 |\n| Deflate/inflate with size limits | flate2 | `jerboa_deflate`, `jerboa_inflate` | \u2705 |\n| Gzip/gunzip with size limits | flate2 | `jerboa_gzip`, `jerboa_gunzip` | \u2705 |\n| Scheme bindings | \u2014 | `(std regex-native)`, `(std compress native-rust)` | \u2705 |\n\nRust: `src/regex_native.rs`, `src/compress.rs`. Scheme: `lib/std/regex-native.sls`, `lib/std/compress/native-rust.sls`. Tests: `tests/test-native-rust.ss`.\n\n### Week 5: Databases \u2705 IMPLEMENTED\n\n| Task | Crate | Functions | Status |\n|------|-------|-----------|--------|\n| SQLite open/close/exec | rusqlite (bundled) | `jerboa_sqlite_open`, `jerboa_sqlite_close`, `jerboa_sqlite_exec` | \u2705 |\n| SQLite prepare/bind/step/column | rusqlite | `jerboa_sqlite_prepare`, `jerboa_sqlite_bind_*`, `jerboa_sqlite_step`, `jerboa_sqlite_column_*` | \u2705 |\n| SQLite reset/finalize/metadata | rusqlite | `jerboa_sqlite_reset`, `jerboa_sqlite_finalize`, `jerboa_sqlite_last_insert_rowid`, `jerboa_sqlite_changes` | \u2705 |\n| PostgreSQL connect/disconnect | rust-postgres | `jerboa_pg_connect`, `jerboa_pg_disconnect` | \u2705 |\n| PostgreSQL exec/query | rust-postgres | `jerboa_pg_exec`, `jerboa_pg_query`, `jerboa_pg_nrows`, `jerboa_pg_ncols` | \u2705 |\n| PostgreSQL result access | rust-postgres | `jerboa_pg_get_value`, `jerboa_pg_is_null`, `jerboa_pg_column_name`, `jerboa_pg_free_result` | \u2705 |\n| Scheme bindings | \u2014 | `(std db sqlite-native)`, `(std db postgresql-native)` | \u2705 |\n\nRust: `src/sqlite.rs`, `src/postgres_native.rs`. Scheme: `lib/std/db/sqlite-native.sls`, `lib/std/db/postgresql-native.sls`. Tests: `tests/test-native-rust-week5-6.ss` (26 tests). SQLite uses raw `sqlite3_*` C API via `rusqlite::ffi` to avoid Rust lifetime issues with Statement handles. PostgreSQL uses handle stores for `Client` and `Vec<Row>`.\n\n### Week 6: OS Integration \u2705 IMPLEMENTED\n\n| Task | Crate | Functions | Status |\n|------|-------|-----------|--------|\n| epoll create/ctl/wait/close | libc | `jerboa_epoll_create`, `jerboa_epoll_ctl`, `jerboa_epoll_wait`, `jerboa_epoll_close` | \u2705 |\n| inotify init/watch/read/close | libc | `jerboa_inotify_init`, `jerboa_inotify_add_watch`, `jerboa_inotify_rm_watch`, `jerboa_inotify_read`, `jerboa_inotify_close` | \u2705 |\n| Landlock ABI/ruleset/rules/enforce | libc (syscalls) | `jerboa_landlock_abi_version`, `jerboa_landlock_create_ruleset`, `jerboa_landlock_add_path_rule`, `jerboa_landlock_add_net_rule`, `jerboa_landlock_enforce` | \u2705 |\n| io_uring | io-uring | \u2014 | \u23f3 Deferred |\n| Scheme bindings | \u2014 | `(std os epoll-native)`, `(std os inotify-native)`, `(std os landlock-native)` | \u2705 |\n\nRust: `src/epoll.rs`, `src/inotify_native.rs`, `src/landlock.rs`. Scheme: `lib/std/os/epoll-native.sls`, `lib/std/os/inotify-native.sls`, `lib/std/os/landlock-native.sls`. Tests: `tests/test-native-rust-week5-6.ss`. Landlock supports ABI v1-v7 with filesystem + network rules. io_uring deferred (complex async interface).\n\n---\n\n## Summary\n\n| Metric | Before (C libraries) | After (Rust native) |\n|--------|---------------------|---------------------|\n| Shared libraries loaded | 10+ | 1 |\n| C code in trust boundary | ~500K lines (OpenSSL + SQLite + ...; libyaml already eliminated via pure Scheme) | 0 (Rust only; C primitives inside ring are audited asm) |\n| Memory safety CVEs possible | Yes \u2014 every C library | No \u2014 Rust compiler prevents them |\n| ReDoS possible | Yes \u2014 PCRE2 backtracks | No \u2014 Rust regex uses NFA |\n| Secret wiping guarantee | No \u2014 GC copies | Yes \u2014 secure region is outside GC |\n| Dependency audit | Manual per-library | `cargo audit` \u2014 automated |\n| Build reproducibility | Varies per library | `Cargo.lock` \u2014 exact pins |\n| Static binary size impact | ~15-25MB (all C libs) | ~5-10MB (LTO, dead code eliminated) |\n"}
 {"text": "<!-- FILE: docs/stm.md -->\n# Software Transactional Memory in Jerboa\n\n`(std stm)` provides optimistic concurrency through transactional variables\n(TVars). Transactions execute speculatively, validate their read-set at\ncommit time, and automatically retry on conflict \u2014 all without requiring the\nprogrammer to acquire or release locks.\n\n---\n\n## Overview\n\nTraditional lock-based concurrency is error-prone: locks must be acquired in\na consistent order to avoid deadlock, held for the right duration to avoid\nraces, and released even when exceptions occur. STM eliminates these problems\nby treating shared state mutations as database transactions:\n\n1. A thread **reads** TVars freely during a transaction. First reads are\n   timestamped.\n2. Writes are buffered in a **write-set** private to the thread.\n3. At the end of `atomically`, the runtime acquires the **global commit\n   mutex** and validates that every TVar read during the transaction still\n   has the same version number it had at the time of the first read.\n4. If valid, writes are applied atomically and version counters are\n   incremented. A broadcast on the commit condition variable wakes any\n   threads blocked in `retry`.\n5. If any version has changed (conflict), the entire transaction is discarded\n   and re-run from the beginning.\n\n**Key properties:**\n\n- **Composable**: two atomic operations can be composed into a larger atomic\n  operation simply by nesting or sequencing inside `atomically`.\n- **Deadlock-free**: there are no locks for application code to acquire; only\n  the internal commit mutex is used, and it is held only for the validation\n  and write phase.\n- **No explicit cleanup**: exceptions inside a transaction leave no partial\n  state behind. The write-set is simply discarded.\n\n---\n\n## Importing the Library\n\n```scheme\n(import (std stm))\n```\n\n---\n\n## Core API\n\n### `make-tvar` \u2014 create a transactional variable\n\n```scheme\n(make-tvar init) => tvar\n```\n\nCreates a new TVar holding `init` as its initial committed value with version\n`0`.\n\n```scheme\n(define balance (make-tvar 1000))\n(define inventory (make-tvar '()))\n```\n\n---\n\n### `tvar?` \u2014 predicate\n\n```scheme\n(tvar? x) => boolean\n```\n\nReturns `#t` if `x` is a TVar.\n\n---\n\n### `tvar-ref` \u2014 read outside a transaction\n\n```scheme\n(tvar-ref tv) => value\n```\n\nReads the current committed value of `tv` without starting a transaction.\nUseful for observation and debugging. **Do not use inside `atomically`** \u2014\nuse `tvar-read` instead, or the read will not be recorded in the\ntransaction's read-set and conflicts will go undetected.\n\n```scheme\n(display (tvar-ref balance))   ; safe outside transactions\n```\n\n---\n\n### `atomically` \u2014 run a transaction\n\n```scheme\n(atomically body ...) => value\n```\n\nEvaluates `body ...` as a single atomic transaction and returns the value of\nthe last expression. The body runs speculatively: if the transaction\nconflicts with a concurrent commit it is re-run automatically. If the body\ncalls `retry`, the thread blocks until at least one TVar in its read-set is\nmodified by another transaction, then re-runs.\n\n**Nesting**: if `atomically` is called while already inside a transaction the\ninner call flattens into the enclosing transaction \u2014 all reads and writes\nparticipate in the same commit.\n\n```scheme\n(atomically\n  (let ([bal (tvar-read balance)])\n    (tvar-write! balance (- bal amount))))\n```\n\n---\n\n### `tvar-read` \u2014 transactional read\n\n```scheme\n(tvar-read tv) => value\n```\n\nReads `tv` inside a transaction. On the first read of a TVar the current\nversion is snapshotted into the transaction's read-set. Subsequent reads of\nthe same TVar return the current committed value (consistency is enforced at\ncommit time, not per-read). If the TVar has been written in the current\ntransaction, the buffered value is returned instead.\n\nCalled outside a transaction, `tvar-read` behaves like `tvar-ref` (direct\nread, no recording).\n\n---\n\n### `tvar-write!` \u2014 transactional write\n\n```scheme\n(tvar-write! tv val)\n```\n\nBuffers a write to `tv` in the current transaction's write-set. The actual\ncommitted value is not changed until the transaction commits successfully.\nMultiple writes to the same TVar within one transaction are coalesced: only\nthe last write is applied.\n\nCalled outside a transaction, `tvar-write!` writes directly with the commit\nmutex held and broadcasts on the condition variable.\n\n---\n\n### `retry` \u2014 block until something changes\n\n```scheme\n(retry)\n```\n\nAborts the current transaction and blocks the calling thread until at least\none TVar that was read during this transaction is modified by another\ncommitted transaction. The thread then re-runs the transaction from the\nbeginning.\n\n`retry` is the STM equivalent of a condition wait, but it is entirely\ndeclarative: you express _when_ to retry (a guard condition is not yet\nsatisfied) and the runtime figures out _what_ to wait for (the TVars whose\nstate determines the condition).\n\n```scheme\n(define (dequeue! q)\n  (atomically\n    (let ([items (tvar-read q)])\n      (if (null? items)\n        (retry)                          ; block until queue is non-empty\n        (begin\n          (tvar-write! q (cdr items))\n          (car items))))))\n```\n\n---\n\n### `or-else` \u2014 alternative on retry\n\n```scheme\n(or-else expr1 expr2)\n```\n\nEvaluates `expr1`. If `expr1` calls `retry`, evaluates `expr2` instead.\nBoth expressions run within the same enclosing transaction if one is active.\n`or-else` allows you to try multiple strategies in priority order, falling\nback gracefully when none is immediately satisfiable.\n\n```scheme\n(define (dequeue-any! high-priority-q low-priority-q)\n  (atomically\n    (or-else\n      (dequeue-inner! high-priority-q)   ; try high-priority first\n      (dequeue-inner! low-priority-q)))) ; fall back to low-priority\n```\n\n---\n\n## How It Works Internally\n\n### TVar structure\n\nEach TVar holds:\n- `stm-value`: the current committed value\n- `stm-version`: a monotonically increasing integer, incremented on every\n  successful write\n\n### Transaction record\n\nEach transaction maintains:\n- `tx-read-set`: association list of `(tvar . version-at-first-read)`\n- `tx-write-set`: association list of `(tvar . new-value)`\n\nThe read-set records the version seen the first time each TVar is read. The\nwrite-set buffers pending writes.\n\n### Commit protocol\n\n1. Acquire `*commit-mutex*` (single global mutex).\n2. Walk the read-set. For each entry, compare the TVar's current version\n   against the snapshotted version. If any has changed: **conflict** \u2014\n   release mutex, discard state, re-run.\n3. If all versions match: apply the write-set. For each written TVar,\n   update `stm-value` and increment `stm-version`.\n4. If any writes were applied, broadcast on `*commit-cond*` to wake threads\n   blocked in `retry`.\n5. Release the mutex.\n\n### Retry blocking\n\nWhen `retry` is called:\n1. The `&stm-retry` condition is raised.\n2. `atomically` catches it, acquires the commit mutex, and calls\n   `condition-wait` on `*commit-cond*`.\n3. When any transaction commits with writes, `condition-broadcast` wakes all\n   waiting threads.\n4. The thread re-acquires the mutex, releases it, and re-runs the entire\n   transaction.\n\nThis is a coarse wake-up: all threads sleeping in `retry` wake when _any_\nTVar changes, even one they did not read. The transaction's own guard will\nsimply call `retry` again if the relevant TVar still has an unsatisfactory\nvalue, and the thread will sleep again.\n\n---\n\n## Complete Examples\n\n### Bank account transfer\n\nA classic problem: transfer funds between two accounts atomically. Without\nSTM this requires careful lock ordering. With STM you just read and write.\n\n```scheme\n(import (chezscheme) (std stm))\n\n(define account-a (make-tvar 500))\n(define account-b (make-tvar 300))\n\n(define (transfer! from to amount)\n  (atomically\n    (let ([from-bal (tvar-read from)])\n      (when (< from-bal amount)\n        (error 'transfer! \"insufficient funds\" from-bal amount))\n      (tvar-write! from (- from-bal amount))\n      (tvar-write! to   (+ (tvar-read to) amount)))))\n\n(transfer! account-a account-b 200)\n(display (tvar-ref account-a))   ; => 300\n(display (tvar-ref account-b))   ; => 500\n```\n\nThe read of `from` and the read of `to` are both in the same transaction's\nread-set. If another thread modifies either account concurrently, the commit\nwill fail and the entire transfer re-runs \u2014 the accounts are always\nconsistent.\n\n---\n\n### Bounded queue with STM\n\nA thread-safe bounded queue where producers block when full and consumers\nblock when empty.\n\n```scheme\n(import (chezscheme) (std stm))\n\n(define (make-stm-queue capacity)\n  (cons (make-tvar '())         ; items (as a list)\n        (make-tvar capacity)))  ; remaining capacity\n\n(define (queue-push! q item)\n  (let ([items-tv (car q)]\n        [cap-tv   (cdr q)])\n    (atomically\n      (let ([cap (tvar-read cap-tv)])\n        (when (fxzero? cap)\n          (retry))                           ; block until space is available\n        (tvar-write! items-tv (append (tvar-read items-tv) (list item)))\n        (tvar-write! cap-tv   (fx- cap 1))))))\n\n(define (queue-pop! q)\n  (let ([items-tv (car q)]\n        [cap-tv   (cdr q)])\n    (atomically\n      (let ([items (tvar-read items-tv)])\n        (when (null? items)\n          (retry))                           ; block until item is available\n        (tvar-write! items-tv (cdr items))\n        (tvar-write! cap-tv   (fx+ (tvar-read cap-tv) 1))\n        (car items)))))\n\n;; Usage\n(define q (make-stm-queue 10))\n\n(fork-thread (lambda ()\n  (let loop ([i 0])\n    (queue-push! q i)\n    (loop (+ i 1)))))\n\n(fork-thread (lambda ()\n  (let loop ()\n    (display (queue-pop! q))\n    (loop))))\n```\n\n---\n\n### Dining philosophers without deadlock\n\nThe classic dining philosophers problem has no deadlock with STM because\nthere are no locks to acquire in order.\n\n```scheme\n(import (chezscheme) (std stm))\n\n;; Five forks; each TVar holds #t = in use, #f = available\n(define fork-count 5)\n(define forks-tv (map make-tvar (make-list fork-count #f)))\n\n(define (pick-up-forks! i)\n  (let ([left  (list-ref forks-tv i)]\n        [right (list-ref forks-tv (modulo (+ i 1) fork-count))])\n    (atomically\n      (when (or (tvar-read left) (tvar-read right))\n        (retry))                   ; both must be available simultaneously\n      (tvar-write! left  #t)\n      (tvar-write! right #t))))\n\n(define (put-down-forks! i)\n  (let ([left  (list-ref forks-tv i)]\n        [right (list-ref forks-tv (modulo (+ i 1) fork-count))])\n    (atomically\n      (tvar-write! left  #f)\n      (tvar-write! right #f))))\n\n(define (philosopher i)\n  (let loop ()\n    (pick-up-forks! i)\n    (display (string-append \"Philosopher \" (number->string i) \" eating\\n\"))\n    (put-down-forks! i)\n    (loop)))\n\n(for-each (lambda (i) (fork-thread (lambda () (philosopher i))))\n          '(0 1 2 3 4))\n```\n\nNo deadlock is possible: `retry` inside `pick-up-forks!` blocks until\n_both_ forks are simultaneously available, and the runtime handles the\nwaiting without any locks in application code.\n\n---\n\n### Priority scheduling with `or-else`\n\n```scheme\n(import (chezscheme) (std stm))\n\n(define urgent-queue  (make-tvar '()))\n(define normal-queue  (make-tvar '()))\n\n(define (enqueue! tv item)\n  (atomically (tvar-write! tv (append (tvar-read tv) (list item)))))\n\n(define (dequeue-inner! tv)\n  ;; Must be called inside an enclosing atomically\n  (let ([items (tvar-read tv)])\n    (if (null? items)\n      (retry)\n      (begin (tvar-write! tv (cdr items)) (car items)))))\n\n(define (next-task!)\n  (atomically\n    (or-else\n      (dequeue-inner! urgent-queue)   ; always prefer urgent work\n      (dequeue-inner! normal-queue))))\n\n;; If urgent-queue is empty, dequeue-inner! calls retry and or-else\n;; falls through to normal-queue. If both are empty, the whole\n;; next-task! call blocks until either queue has an item.\n```\n\n---\n\n## Performance\n\n### When STM shines\n\n- **Low contention**: when threads rarely conflict, transactions commit on\n  the first attempt and the overhead is one read-set validation pass plus a\n  mutex acquisition. For workloads with few writers and many readers this\n  is very efficient.\n- **Composability**: combining two independently correct atomic operations\n  into a larger atomic operation requires no redesign. With locks you would\n  need to expose and coordinate the internal locks, risking deadlock.\n- **Complex multi-variable invariants**: maintaining invariants across\n  several TVars is natural \u2014 either all writes commit or none do.\n\n### When to prefer traditional locking\n\n- **High contention with large write-sets**: when many threads frequently\n  write to the same TVars, conflict rates rise and transactions must re-run\n  repeatedly. A dedicated mutex or channel may perform better.\n- **Very tight inner loops**: the read-set allocation (an assoc list) adds\n  GC pressure. For sub-microsecond hot paths, fixnum-only `mutex` sections\n  may be faster.\n- **Write-heavy single-variable updates**: if you are just atomically\n  incrementing a counter, a `mutex`-guarded increment is simpler and\n  marginally faster than a full transaction.\n\n---\n\n## Pitfalls\n\n### Side effects inside transactions\n\nTransactions may run **more than once**. Any side effect in the body \u2014\nprinting, writing to a file, sending a network packet \u2014 will be repeated on\neach retry. Place side effects after `atomically` returns, not inside it.\n\n```scheme\n;; WRONG: display may run multiple times\n(atomically\n  (display \"withdrawing...\")      ; runs on every retry\n  (tvar-write! balance (- (tvar-read balance) amount)))\n\n;; CORRECT: side effect after the transaction\n(atomically\n  (tvar-write! balance (- (tvar-read balance) amount)))\n(display \"withdrawal complete\")\n```\n\n### `tvar-ref` inside transactions\n\nUsing `tvar-ref` inside `atomically` bypasses the read-set recording. The\nvalue is read from committed state but the version is not snapshotted, so\nconflicting concurrent writes will not be detected at commit time. Always use\n`tvar-read` inside transactions.\n\n```scheme\n;; WRONG: conflict on balance will not be caught\n(atomically\n  (let ([bal (tvar-ref balance)])   ; not recorded in read-set!\n    (tvar-write! balance (- bal 10))))\n\n;; CORRECT\n(atomically\n  (let ([bal (tvar-read balance)])\n    (tvar-write! balance (- bal 10))))\n```\n\n### Infinite retry loops\n\nIf the condition guarded by `retry` can never become true, the thread will\nblock forever. Ensure that some other thread can plausibly modify the TVars\nin the read-set and satisfy the guard.\n\n### Coarse-grained wakeup\n\nAll threads sleeping in `retry` are woken whenever any transaction commits\nwith writes, not just those waiting for the specific TVars they read.\nHigh-contention systems with many `retry`-blocked threads may see spurious\nwakeups. Each spurious wakeup results in a transaction re-run that calls\n`retry` again, so correctness is not affected \u2014 only efficiency. For very\nlarge numbers of blocked threads, consider partitioning TVars into smaller\ngroups or using more targeted condition variables.\n\n### `or-else` and write-set accumulation\n\nBoth branches of `or-else` run within the same enclosing transaction if\none exists. Writes buffered by a branch that subsequently calls `retry` are\n_not_ rolled back before the alternative branch runs. Design `or-else`\nbranches to be independent or idempotent with respect to their writes.\n"}
@@ -349,7 +349,7 @@
 {"text": "<!-- FILE: docs/migration.md -->\n# Migrating from Gerbil to Jerboa\n\nThis guide covers porting Gerbil Scheme code to Jerboa (stock Chez Scheme).\n\n## What Works Unchanged\n\nMost Gerbil user-level code runs on Jerboa with only import changes:\n\n- `def`, `def*` with optional, keyword, and rest arguments\n- `defstruct`, `defclass`, `defmethod`\n- `match` (lists, cons, predicates, guards, `and`/`or`/`not`)\n- `try`/`catch`/`finally`\n- `while`, `until`, `when`, `unless`\n- `defrule`, `defrules`\n- Hash tables: `hash-ref`, `hash-put!`, `hash-get`, `hash-keys`, etc.\n- Method dispatch: `{method obj}` or `(~ obj 'method)`\n- `chain`, `chain-and`, `assert!`\n- `displayln`, `iota`, `1+`, `1-`\n- JSON: `read-json`, `write-json`\n- Path ops: `path-expand`, `path-join`, `path-directory`\n- String ops: `string-split`, `string-join`, `string-trim`\n- List ops: `take`, `drop`, `any`, `every`, `filter-map`\n\n## Import Translation\n\n### Module Paths\n\nGerbil's `:std/foo` becomes Jerboa's `(std foo)`:\n\n```scheme\n;; Gerbil\n(import :std/text/json :std/sort :std/misc/string)\n\n;; Jerboa\n(import (std text json) (std sort) (std misc string))\n```\n\nOr use the prelude for the most common modules:\n\n```scheme\n;; Gerbil\n(import :std/sugar :std/sort :std/format :std/text/json\n        :std/misc/string :std/misc/list :std/misc/alist :std/misc/ports)\n\n;; Jerboa \u2014 all of the above in one import\n(import (jerboa prelude))\n```\n\n### Gambit Compatibility\n\nFor code that uses Gambit primitives (`##` namespace, u8vector ops, etc.):\n\n```scheme\n;; Gerbil\n(import :gerbil/gambit)\n\n;; Jerboa\n(import (std gambit-compat))\n```\n\nThis provides 150+ compatibility symbols including `u8vector` ops, `void?`,\n`call-with-input-string`, `cpu-count`, and more.\n\n### Import Conflicts\n\nThe #1 friction point. Chez Scheme and Jerboa define some of the same names\nwith different semantics. See [import-conflicts.md](import-conflicts.md) for\nthe full matrix.\n\n**Quick solution \u2014 use `(jerboa prelude clean)`:**\n\n```scheme\n;; No conflicts \u2014 prelude/clean excludes names that collide with (chezscheme)\n(import (chezscheme) (jerboa prelude clean))\n```\n\n**Manual solution \u2014 use `except`:**\n\n```scheme\n(import (except (chezscheme)\n          make-hash-table hash-table?\n          sort sort! format printf fprintf\n          iota 1+ 1-\n          path-extension path-absolute?\n          with-input-from-string with-output-to-string)\n        (jerboa prelude))\n```\n\n## Mechanical Transformations\n\n### Keyword Arguments\n\nGerbil uses `#:keyword` syntax; Jerboa uses quoted symbols:\n\n```scheme\n;; Gerbil\n(http-get url headers: my-headers timeout: 30)\n\n;; Jerboa\n(http-get url headers: my-headers timeout: 30)\n;; Jerboa's `def` macro supports keyword: syntax natively\n```\n\n### `spawn` and Threading\n\n```scheme\n;; Gerbil\n(import :std/sugar)\n(spawn (lambda () (displayln \"hello\")))\n(spawn/name \"worker\" (lambda () (work)))\n\n;; Jerboa\n(import (std misc thread))\n(spawn (lambda () (displayln \"hello\")))\n(spawn/name \"worker\" (lambda () (work)))\n```\n\n### Channels\n\n```scheme\n;; Gerbil\n(import :std/misc/channel)\n(def ch (make-channel))\n(channel-put ch 42)\n(channel-get ch)\n\n;; Jerboa\n(import (std misc channel))\n(def ch (make-channel))\n(channel-put ch 42)\n(channel-get ch)\n```\n\n### Iterators\n\n```scheme\n;; Gerbil\n(import :std/iter)\n(for/collect ((x (in-range 10))) (* x x))\n\n;; Jerboa\n(import (std iter))\n(for/collect ((x (in-range 10))) (* x x))\n```\n\n### Actors\n\n```scheme\n;; Gerbil\n(import :std/actor)\n(defproto myproto (greeting name))\n\n;; Jerboa\n(import (std actor))\n(defprotocol myproto (greeting name))\n```\n\n## What Needs Redesign\n\n### Gerbil Expander API\n\nCode that uses `:gerbil/expander` (custom syntax transformers, module\nintrospection) has no Jerboa equivalent. This is internal Gerbil machinery,\nnot user-facing API. Alternatives:\n\n- Use Chez's `syntax-case` directly for macro work\n- Use `(std doc)` for module introspection\n\n### `##` Gambit Primitives\n\nMost `##` primitives are covered by `(std gambit-compat)`. For the rest,\ncheck `(chezscheme)` for native equivalents:\n\n| Gambit | Chez Scheme |\n|--------|-------------|\n| `##sys-clock` | `(cpu-time)` |\n| `##gc` | `(collect)` |\n| `##void` | `(void)` |\n| `##fixnum?` | `(fixnum? x)` |\n| `##car` | `(car x)` (Chez already inlines) |\n\n### `(export #t)` \u2014 Re-export All\n\nGerbil's `(export #t)` re-exports everything. Chez R6RS requires explicit\nexports. You must list all exported symbols in the library form.\n\n### `parameterize` Thread Locality\n\nGerbil's `parameterize` is not thread-local. Chez's `parameterize` IS\nthread-local (each thread gets its own parameter cell). This usually doesn't\nmatter, but if your code relies on one thread's `parameterize` being visible\nto another, you need explicit communication (channels, shared state).\n\n## The Source Translator\n\nFor bulk porting, Jerboa includes a source translator:\n\n```scheme\n(import (jerboa translator))\n\n;; Translate a Gerbil source file\n(translate-file \"input.ss\" \"output.ss\" (default-transforms))\n```\n\nThis handles:\n- `#:keyword` syntax normalization\n- `[x y]` bracket form translation\n- `##gambit-primitive` replacement\n- `defstruct` \u2192 R6RS record expansion\n- `let-hash` / `using` expansion\n- Import path translation\n- `try`/`catch` normalization\n\n## Common Pitfalls\n\n1. **Forgetting import exclusions** \u2014 If you get \"duplicate import\" errors,\n   check [import-conflicts.md](import-conflicts.md).\n\n2. **`match` on structs** \u2014 Core `match` doesn't destructure structs.\n   Import `(std match2)` for struct patterns with `define-match-type`.\n\n3. **`format` differences** \u2014 Jerboa's `format` returns a string (like\n   Gerbil). Chez's `format` writes to a port. If you import both, exclude\n   one.\n\n4. **`sort` stability** \u2014 Jerboa's `sort` is NOT guaranteed stable. Use\n   `stable-sort` when order of equal elements matters.\n\n5. **Thread primitives** \u2014 Chez's `fork-thread` works but Gerbil's `spawn`\n   API (with names, groups) is in `(std misc thread)`.\n"}
 {"text": "<!-- FILE: docs/TODO.md -->\n# Jerboa TODO\n\nUpdated 2026-03-21.\n\nTracks remaining work. Items marked DONE were either already implemented\nor completed in the March 2026 sprint. Remaining items are genuinely open.\n\n---\n\n## Completed\n\n### Developer Experience\n- [x] **CLI entry point** \u2014 `bin/jerboa` with run, test, eval, repl, build, version (`bin/jerboa`)\n- [x] **Error messages** \u2014 `(std errors)` with \"did you mean\" suggestions, Levenshtein distance, `install-error-handler!`\n- [x] **Import conflict ergonomics** \u2014 `(jerboa prelude clean)` conflict-free prelude + `docs/import-conflicts.md` reference\n\n### Language Features\n- [x] **String interpolation** \u2014 `(std interpolate)` macro: `(interpolate \"Hello ${name}\")`\n- [x] **Struct pattern matching** \u2014 `(std match2)` with `define-match-type`, sealed hierarchies, exhaustiveness\n- [x] **Iterator protocol** \u2014 `(std iter)` with `for/collect`, `for/fold`, `in-range`, `in-hash-*`, etc.\n- [x] **Interface/protocol system** \u2014 `(std interface)` with `definterface` + `(std generic)` with `defgeneric`\n\n### Standard Library\n- [x] **SRFI-1** (list library, 271 lines), **SRFI-13** (string library, 258 lines), **SRFI-14** (character sets), **SRFI-19** (date/time), **SRFI-43** (vectors), **SRFI-128** (comparators), **SRFI-141** (integer division)\n- [x] **Process management** \u2014 `(std misc process)` with `process-port-pid`, `process-kill`, `tty?`\n- [x] **Spawn/concurrency** \u2014 `(std misc thread)` with `spawn`, `spawn/name`, `spawn/group`, `thread-sleep!`\n- [x] **CLI framework** \u2014 `(std cli multicall)` subcommands, `(std cli style)` ANSI colors, `(std cli completion)` bash/zsh\n- [x] **Serialization** \u2014 `(std text toml)`, `(std text msgpack)`, `(std text cbor)`, `(std io)` structured I/O\n- [x] **Networking** \u2014 `(std net smtp)`, `(std net json-rpc)`, `(std net udp)`, `(std net address)`\n\n### Ecosystem & Tooling\n- [x] **Package manager** \u2014 `(jerboa pkg)` with semver + `(jerboa lock)` with lockfiles\n- [x] **Build system** \u2014 `(std build)` with module discovery, DAG ordering, content hashing\n- [x] **Source translator** \u2014 `(jerboa translator)` with 20+ transform functions\n\n### Validation & Trust\n- [x] **CI pipeline** \u2014 `.github/workflows/test.yml` (GitHub Actions)\n- [x] **Benchmark suite** \u2014 `benchmarks/bench-core.ss` (hash tables, match, sort, JSON, iterators, structs)\n- [x] **Example applications** \u2014 `examples/hello-api.ss`, `examples/cli-tool.ss`, `examples/data-pipeline.ss`, `examples/chat-server.ss`\n- [x] **Ports documentation** \u2014 `docs/ports.md`\n\n### Documentation\n- [x] **Quickstart guide** \u2014 `docs/quickstart.md`\n- [x] **Migration guide** \u2014 `docs/migration.md`\n- [x] **Import conflict reference** \u2014 `docs/import-conflicts.md`\n- [x] **API reference generator** \u2014 `tools/gen-api-docs.ss`\n\n- [x] **Struct patterns in prelude match** \u2014 `(jerboa prelude)` now re-exports `(std match2)`'s `match` with struct patterns, sealed hierarchies, active patterns, and `match/strict`\n- [x] **Package registry** \u2014 `(jerboa registry)` with GitHub-based install/uninstall/update; `bin/jerboa install/uninstall/update/list` CLI commands\n- [x] **LSP server** \u2014 `(std lsp server)` + `(std lsp symbols)` with JSON-RPC over stdio; `tools/jerboa-lsp.ss` entry point; completion, hover, go-to-definition, diagnostics\n- [x] **Protocol Buffers** \u2014 `(std protobuf)` wire format encoder/decoder (proto3); varint, fixed32/64, length-delimited, zigzag, embedded messages\n\n---\n\n## Remaining \u2014 Gerbil Parity Sprint (50 features)\n\nFeatures that Gerbil has but Jerboa does not. Organized by category.\n\n### Core Language (4)\n\n- [x] **1. `(std amb)`** \u2014 Nondeterministic computation with backtracking (amb, amb-find, amb-collect, amb-assert)\n- [x] **2. `(std lazy)`** \u2014 Lazy streams (lazy, delay, force, lcons, ltake, ldrop, lmap, lfilter, lfold, lappend)\n- [x] **3. `(std ref)`** \u2014 Generic polymorphic accessor (ref, ref-set! for lists, vectors, hash tables, strings)\n- [x] **4. `(std deprecation)`** \u2014 Deprecation warnings with source location and migration hints\n\n### Data Structures (7)\n\n- [x] **5. `(std misc plist)`** \u2014 Property lists (pget, pput, pdel, plist->alist, alist->plist, plist-keys)\n- [x] **6. `(std misc evector)`** \u2014 Expandable/growable vectors (make-evector, evector-push!, evector-pop!, evector-ref, evector->vector)\n- [x] **7. `(std misc rbtree)`** \u2014 Red-black balanced BST (rbtree-insert, rbtree-lookup, rbtree-delete, rbtree-fold, rbtree->list)\n- [x] **8. `(std misc decimal)`** \u2014 Exact decimal arithmetic (decimal+, decimal*, decimal/, decimal-round, string->decimal, decimal->string)\n- [x] **9. `(std misc prime)`** \u2014 Primality testing, prime generation, factorization (prime?, next-prime, primes-up-to, factorize)\n- [x] **10. `(std misc dag)`** \u2014 DAG operations (make-dag, dag-add-edge!, topological-sort, dag-reachable, dag-sources, dag-sinks)\n- [x] **11. `(std misc shared)`** \u2014 Thread-safe shared mutable state (make-shared, shared-ref, shared-set!, shared-update!, shared-cas!)\n\n### Text & Encoding (4)\n\n- [x] **12. `(std text utf16)`** \u2014 UTF-16 encoding/decoding (string->utf16, utf16->string, utf16-length, BOM handling)\n- [x] **13. `(std text utf32)`** \u2014 UTF-32 encoding/decoding (string->utf32, utf32->string)\n- [x] **14. `(std text base58)`** \u2014 Base58 encoding for Bitcoin/IPFS (base58-encode, base58-decode, base58check-encode)\n- [x] **15. `(std text html)`** \u2014 HTML parsing and generation (html-escape, html-unescape, parse-html, html->sxml)\n\n### Markup (3)\n\n- [x] **16. `(std markup sxml)`** \u2014 SXML representation, construction, and manipulation (sxml:element?, sxml:attributes, sxml:children)\n- [x] **17. `(std markup sxml-path)`** \u2014 XPath-like queries on SXML trees (sxpath, sxml:select, node-typeof?, node-join)\n- [x] **18. `(std markup sxml-print)`** \u2014 SXML to HTML/XML serialization (sxml->html, sxml->xml, sxml->string)\n\n### Networking (4)\n\n- [x] **19. `(std net socks)`** \u2014 SOCKS4/5 proxy client (socks-connect, socks4-connect, socks5-connect, with-socks-proxy)\n- [x] **20. `(std net s3)`** \u2014 AWS S3 client (s3-put-object, s3-get-object, s3-list-bucket, s3-delete-object, sigv4 signing)\n- [x] **21. `(std net sasl)`** \u2014 SASL authentication mechanisms (sasl-plain, sasl-scram-sha-256, sasl-step)\n- [x] **22. `(std net bio)`** \u2014 Buffered network I/O (make-bio-input, make-bio-output, bio-read-line, bio-read-bytes, bio-flush)\n\n### Web (2)\n\n- [x] **23. `(std web fastcgi)`** \u2014 FastCGI protocol (fastcgi-accept, fastcgi-respond, fastcgi-params)\n- [x] **24. `(std web rack)`** \u2014 Middleware/handler composable web interface (make-app, wrap-middleware, rack-run)\n\n### MIME (2)\n\n- [x] **25. `(std mime struct)`** \u2014 MIME message structure (make-mime-message, mime-headers, mime-body, multipart-encode, multipart-decode)\n- [x] **26. `(std mime types)`** \u2014 MIME type database (extension->mime-type, mime-type->extension, mime-type?)\n\n### Crypto (2)\n\n- [x] **27. `(std crypto bn)`** \u2014 Big number arithmetic (bn+, bn*, bn-mod, bn-expt-mod, bn->bytevector, bytevector->bn)\n- [x] **28. `(std crypto dh)`** \u2014 Diffie-Hellman key exchange (dh-generate-parameters, dh-generate-key, dh-compute-shared)\n\n### Database (2)\n\n- [x] **29. `(std db dbi)`** \u2014 Generic database interface (dbi-connect, dbi-query, dbi-exec, dbi-prepare, dbi-with-transaction)\n- [x] **30. `(std db conpool)`** \u2014 Database connection pooling (make-connection-pool, pool-acquire, pool-release, with-connection)\n\n### Parser Framework (2)\n\n- [x] **31. `(std parser deflexer)`** \u2014 Lexer definition macro (deflexer, define-token, lex-string, lex-port)\n- [x] **32. `(std parser defparser)`** \u2014 Parser definition macro with grammar rules (defparser, define-rule, parse)\n\n### Protobuf Enhanced (2)\n\n- [x] **33. `(std protobuf macros)`** \u2014 defmessage macro for defining proto3 message types as Scheme records\n- [x] **34. `(std protobuf grammar)`** \u2014 .proto file parser (read-proto-file, proto->scheme)\n\n### Debug (3)\n\n- [x] **35. `(std debug heap)`** \u2014 Heap introspection and GC stats (heap-size, gc-count, gc-time, object-counts)\n- [x] **36. `(std debug memleak)`** \u2014 Memory leak detection (track-allocation, report-leaks, with-leak-check)\n- [x] **37. `(std debug threads)`** \u2014 Thread inspection and debugging (thread-list, thread-backtrace, thread-state)\n\n### I/O (3)\n\n- [x] **38. `(std io bio)`** \u2014 Buffered I/O with lookahead (make-buffered-input, buffered-peek, buffered-read-line, buffered-unread)\n- [x] **39. `(std io strio)`** \u2014 String-based I/O with readers/writers (make-string-reader, reader-peek, reader-read-while)\n- [x] **40. `(std io delimited)`** \u2014 Delimited text I/O (read-delimited, read-line*, read-until, write-delimited)\n\n### CLI (1)\n\n- [x] **41. `(std cli print-exit)`** \u2014 Print formatted output and exit (print-exit, print-error-exit, exit/success, exit/failure)\n\n### SRFI (9)\n\n- [x] **42. `(std srfi srfi-41)`** \u2014 Streams: lazy sequences (stream-cons, stream-null, stream-car, stream-cdr, stream-map, stream-filter, stream-fold)\n- [x] **43. `(std srfi srfi-42)`** \u2014 Eager comprehensions (do-ec, list-ec, vector-ec, sum-ec, every?-ec, first-ec)\n- [x] **44. `(std srfi srfi-113)`** \u2014 Sets and bags (set, bag, set-adjoin, set-delete, set-union, set-intersection, set-difference)\n- [x] **45. `(std srfi srfi-121)`** \u2014 Generators (make-coroutine-generator, generator->list, gmap, gfilter, gfold)\n- [x] **46. `(std srfi srfi-132)`** \u2014 Sort libraries (list-sort, list-stable-sort, vector-sort, vector-stable-sort, list-merge, vector-merge)\n- [x] **47. `(std srfi srfi-133)`** \u2014 Extended vector library (vector-unfold, vector-map, vector-for-each, vector-fold, vector-count, vector-index)\n- [x] **48. `(std srfi srfi-134)`** \u2014 Immutable deques (ideque, ideque-front, ideque-back, ideque-add-front, ideque-add-back, ideque-remove-front)\n- [x] **49. `(std srfi srfi-144)`** \u2014 Flonum library (fl+, fl*, flsqrt, flexp, fllog, flsin, flcos, fl=, fl<, flonum constants)\n- [x] **50. `(std srfi srfi-158)`** \u2014 Generators and accumulator (generator, circular-generator, make-accumulator, generator-fold, generator-for-each)\n\n### Networking Polish (existing)\n\n- **HTTP client API compat** \u2014 header format differences vs Gerbil\n"}
 {"text": "<!-- FILE: docs/typing.md -->\n# Gradual Typing in Jerboa\n\n`(std typed)` and `(std typed advanced)` provide optional type annotations\nfor Chez Scheme code. Annotations compile to runtime assertions in `debug`\nmode and disappear entirely in `release` mode \u2014 zero overhead in production.\n\n---\n\n## Overview\n\nGradual typing lets you add type information incrementally. You can annotate\na single hot function, a whole module, or nothing at all. Untyped and typed\ncode interoperate freely: a typed function can call an untyped one and vice\nversa.\n\nThe philosophy is practical:\n\n- **Development**: catch type errors early with descriptive messages that\n  name the function, the argument, the expected type, and the actual value.\n- **Production**: flip `(*typed-mode* 'release)` and every annotation\n  vanishes from the compiled code. No allocation, no branches, no overhead.\n- **Incrementally adoptable**: annotate what matters, leave the rest alone.\n\n---\n\n## Importing the Library\n\n```scheme\n(import (std typed))\n```\n\nFor advanced features (occurrence typing, row polymorphism, refinement\ntypes, type-directed compilation):\n\n```scheme\n(import (std typed)\n        (std typed advanced))\n```\n\n---\n\n## Core API \u2014 `(std typed)`\n\n### `*typed-mode*`\n\nA Chez Scheme parameter that controls whether annotations are enforced.\n\n```scheme\n(*typed-mode*)           ; => 'debug   (default)\n(*typed-mode* 'debug)    ; enable runtime checks\n(*typed-mode* 'release)  ; strip all checks (zero overhead)\n(*typed-mode* 'none)     ; alias for 'release\n```\n\nAccepts exactly `debug`, `release`, or `none`; any other value raises an\nerror. Set this once at program startup, typically from a command-line flag\nor environment variable.\n\n---\n\n### `define/t` \u2014 typed define\n\n```\n(define/t (name [arg : type] ...) : ret-type body ...)\n(define/t (name [arg : type] ...) body ...)\n```\n\nDefines a function with annotated arguments and an optional return type.\nArguments without a `: type` annotation are implicitly typed `any` (no\ncheck). In `debug` mode the macro expands to argument checks before the body\nand a return-type check after. In `release` mode it expands to a plain\n`define`.\n\n```scheme\n(import (std typed))\n\n;; Both arguments and return type annotated\n(define/t (add-strings [a : string] [b : string]) : string\n  (string-append a b))\n\n;; Only arguments annotated (no return type check)\n(define/t (safe-divide [n : number] [d : number])\n  (if (zero? d)\n    (error 'safe-divide \"division by zero\")\n    (/ n d)))\n\n;; Mixed: one typed, one untyped\n(define/t (scale [xs : list] factor)\n  (map (lambda (x) (* x factor)) xs))\n```\n\nError message example (in `debug` mode):\n\n```\nException in add-strings: b: expected string, got 42\n```\n\n---\n\n### `lambda/t` \u2014 typed lambda\n\n```\n(lambda/t ([arg : type] ...) : ret-type body ...)\n(lambda/t ([arg : type] ...) body ...)\n```\n\nSame as `define/t` but produces an anonymous procedure.\n\n```scheme\n(define process\n  (lambda/t ([items : list] [transform : procedure]) : list\n    (map transform items)))\n\n;; Works naturally as a higher-order argument\n(sort items (lambda/t ([a : string] [b : string]) : boolean\n              (string<? a b)))\n```\n\n---\n\n### `assert-type` \u2014 inline type assertion\n\n```\n(assert-type expr type-name)\n```\n\nEvaluates `expr`, checks that its value satisfies the named type in `debug`\nmode, and returns the value. Useful for asserting types at arbitrary points\nin code without annotating a whole function.\n\n```scheme\n(define (read-config path)\n  (let ([raw (call-with-input-file path read)])\n    (assert-type raw list)     ; must be a list at the top level\n    (process-config raw)))\n```\n\n---\n\n### Built-in Type Names\n\nThe following type names are recognized out of the box:\n\n| Name          | Predicate         |\n|---------------|-------------------|\n| `fixnum`      | `fixnum?`         |\n| `flonum`      | `flonum?`         |\n| `string`      | `string?`         |\n| `pair`        | `pair?`           |\n| `vector`      | `vector?`         |\n| `bytevector`  | `bytevector?`     |\n| `boolean`     | `boolean?`        |\n| `char`        | `char?`           |\n| `symbol`      | `symbol?`         |\n| `list`        | `list?`           |\n| `number`      | `number?`         |\n| `integer`     | `integer?`        |\n| `real`        | `real?`           |\n| `any`         | _(no check)_      |\n\n#### Compound type specs\n\nIn addition to flat names, `type-predicate` understands these constructors:\n\n```scheme\n(listof string)          ; list whose every element is a string\n(vectorof fixnum)        ; vector whose every element is a fixnum\n(hashof symbol any)      ; hashtable (key/value types not checked per-entry)\n(-> string number)       ; any procedure (only arity/type not verified at runtime)\n```\n\n---\n\n### `register-type-predicate!` \u2014 extend the type registry\n\n```scheme\n(register-type-predicate! 'port port?)\n(register-type-predicate! 'positive-integer\n  (lambda (x) (and (integer? x) (positive? x))))\n```\n\nAfter registration the name works everywhere: `define/t`, `lambda/t`,\n`assert-type`, `check-type!`, etc.\n\n---\n\n### `type-predicate` \u2014 look up a predicate\n\n```scheme\n(type-predicate 'string)      ; => string?\n(type-predicate '(listof fixnum))  ; => compound predicate procedure\n(type-predicate 'unknown)     ; => #f\n```\n\nReturns `#f` for unrecognized type specs.\n\n---\n\n### `check-type!` and `check-return-type!` \u2014 procedural checks\n\n```scheme\n(check-type! who arg-name val type-name)\n(check-return-type! who val type-name)\n```\n\nThe primitive checkers used internally by `define/t` and `lambda/t`. You can\ncall them directly to build custom validation helpers. Both are no-ops when\n`(*typed-mode*)` is not `debug`.\n\n```scheme\n(define (validate-config! cfg)\n  (check-type! 'validate-config! 'cfg cfg 'list)\n  (for-each (lambda (entry)\n              (check-type! 'validate-config! 'entry entry 'pair))\n            cfg))\n```\n\n---\n\n### `with-fixnum-ops` \u2014 fixnum arithmetic specialization\n\n```\n(with-fixnum-ops body ...)\n```\n\nA syntax transformer that walks `body` at macro-expansion time and replaces\ngeneric arithmetic operators with their fixnum-specific variants:\n\n| Generic    | Fixnum    |\n|------------|-----------|\n| `+`        | `fx+`     |\n| `-`        | `fx-`     |\n| `*`        | `fx*`     |\n| `/`        | _(none)_  |\n| `<`        | `fx<`     |\n| `>`        | `fx>`     |\n| `<=`       | `fx<=`    |\n| `>=`       | `fx>=`    |\n| `=`        | `fx=`     |\n| `quotient` | `fxquotient` |\n| `remainder`| `fxremainder` |\n| `modulo`   | `fxmodulo` |\n| `abs`      | `fxabs`   |\n| `zero?`    | `fxzero?` |\n| `positive?`| `fxpositive?` |\n| `negative?`| `fxnegative?` |\n| `min`      | `fxmin`   |\n| `max`      | `fxmax`   |\n| `add1`     | `fx1+`    |\n| `sub1`     | `fx1-`    |\n\n```scheme\n(define (sum-fixnums xs)\n  (with-fixnum-ops\n    (let loop ([lst xs] [acc 0])\n      (if (null? lst)\n        acc\n        (loop (cdr lst) (+ acc (car lst)))))))\n```\n\nSpecial forms (`if`, `let`, `lambda`, `cond`, `when`, etc.) are traversed\nbut their heads are never rewritten. Only operator positions are affected.\n\n---\n\n### `with-flonum-ops` \u2014 flonum arithmetic specialization\n\n```\n(with-flonum-ops body ...)\n```\n\nSame idea as `with-fixnum-ops`, replacing generic operators with their\nflonum variants (`fl+`, `fl-`, `fl*`, `fl/`, `flsqrt`, `flsin`, `flcos`,\netc.). Includes transcendentals: `sqrt`, `floor`, `ceiling`, `round`,\n`truncate`, `sin`, `cos`, `tan`, `exp`, `log`.\n\n```scheme\n(define (euclidean-distance x1 y1 x2 y2)\n  (with-flonum-ops\n    (let ([dx (- x2 x1)]\n          [dy (- y2 y1)])\n      (sqrt (+ (* dx dx) (* dy dy))))))\n```\n\n---\n\n### Effect type annotations \u2014 `define/te` and `lambda/te`\n\n```\n(define/te (name [arg : type] ...) : (Effect EffectName ReturnType) body ...)\n(lambda/te ([arg : type] ...) : (Effect EffectName ReturnType) body ...)\n```\n\nEffect types document that a function performs a named side effect and\nreturns a value of `ReturnType`. At runtime only the argument types and\n`ReturnType` are checked; `EffectName` is informational for tooling and\ndocumentation.\n\n```scheme\n(define/te (write-record! [db : any] [rec : pair]) : (Effect IO boolean)\n  (db-insert! db rec))\n\n(define/te (fetch-user [id : fixnum]) : (Effect IO pair)\n  (db-query user-table id))\n```\n\nFallback: if no `(Effect ...)` wrapper is present, `define/te` behaves\nexactly like `define/t`.\n\n#### Effect type descriptors\n\n```scheme\n(make-effect-type effect-name result-type) ; => effect-type record\n(effect-type? x)                           ; => #t/#f\n(effect-type-effect et)                    ; => effect-name symbol\n(effect-type-result et)                    ; => result type symbol\n```\n\nThese records are available for tooling that wants to inspect annotations at\nruntime, but are not used internally by the assertion machinery.\n\n---\n\n## Advanced API \u2014 `(std typed advanced)`\n\n### Step 14: Occurrence Typing\n\nOccurrence typing narrows the type of a variable inside a conditional branch\nbased on the test predicate. The narrowing is inserted as an `assert-type`\ncall at the start of the branch body, making the type-checker (and the\nreader) aware that the variable is more specific within that branch.\n\nSupported narrowings:\n\n| Predicate      | Narrowed type  |\n|----------------|----------------|\n| `string?`      | `string`       |\n| `fixnum?`      | `fixnum`       |\n| `flonum?`      | `flonum`       |\n| `number?`      | `number`       |\n| `integer?`     | `integer`      |\n| `real?`        | `real`         |\n| `pair?`        | `pair`         |\n| `list?`        | `list`         |\n| `null?`        | `null`         |\n| `vector?`      | `vector`       |\n| `symbol?`      | `symbol`       |\n| `char?`        | `char`         |\n| `boolean?`     | `boolean`      |\n| `procedure?`   | `procedure`    |\n| `bytevector?`  | `bytevector`   |\n| `hashtable?`   | `hashtable`    |\n\n#### `if/t`\n\n```scheme\n(if/t test then else)\n```\n\nIn the `then` branch, if `test` is `(pred? var)` and `pred?` maps to a\nknown type, `var` is rebound with an `assert-type` annotation.\n\n```scheme\n(define (stringify x)\n  (if/t (string? x)\n    (string-append \"[\" x \"]\")   ; x narrowed to string here\n    (number->string x)))\n```\n\n#### `when/t`\n\n```scheme\n(when/t test body ...)\n```\n\nLike `if/t` but for one-armed conditionals.\n\n```scheme\n(define (maybe-upcase x)\n  (when/t (string? x)\n    (string-upcase x)))   ; x narrowed to string in body\n```\n\n#### `unless/t`\n\n```scheme\n(unless/t test body ...)\n```\n\nNegated form. The body runs when `test` is false. (Internally expands to\n`when/t (not test) ...`.)\n\n#### `cond/t`\n\n```scheme\n(cond/t ([test body ...] ...)\n        [else body ...])\n```\n\nMulti-branch version. Each clause whose test is a predicate application\nnarrows the tested variable in its body.\n\n```scheme\n(define (describe x)\n  (cond/t\n    [(string? x)  (string-append \"string: \" x)]\n    [(fixnum? x)  (string-append \"fixnum: \" (number->string x))]\n    [(pair? x)    (string-append \"pair of length \" (number->string (length x)))]\n    [else         \"unknown\"]))\n```\n\n---\n\n### Step 15: Row Polymorphism\n\nRow types express structural constraints: \"this object must have at least\nthese fields accessible by these accessors, with these types.\" Any record\ntype that satisfies the row shape passes the check, regardless of its\nnominal type.\n\n#### `defrow`\n\n```\n(defrow Name (field-name : Type) ...)\n```\n\nDefines a named row type and generates:\n\n- `Name?` \u2014 predicate that returns `#t` if the object satisfies the row\n- `check-Name!` \u2014 assertion that raises an error on failure\n\nField accessibility is checked at runtime by calling the accessor (resolved\nvia `eval` in the interaction environment) on the object and catching any\nexception.\n\n```scheme\n(defrow Printable\n  (to-string : procedure))\n\n(defrow Drawable\n  (draw!   : procedure)\n  (x       : number)\n  (y       : number))\n```\n\nAfter this:\n\n```scheme\n(Printable? my-obj)   ; => #t or #f\n(check-Printable! my-obj)  ; raises if not satisfied\n```\n\n#### `row-check`\n\n```\n(row-check obj-expr row-name)\n(row-check obj-expr (field-name : Type) ...)\n```\n\nInline row check without needing a named `defrow`. Returns `#t` or `#f`.\n\n```scheme\n;; Named row\n(row-check widget Drawable)\n\n;; Anonymous inline row\n(row-check config\n  (host : string)\n  (port : fixnum))\n```\n\n#### `row?`\n\n```scheme\n(row? 'Printable)   ; => row-type record or #f\n```\n\nLooks up a named row type from the registry.\n\n#### `row-type?`\n\nPredicate for the `row-type` record type itself (distinguishes row-type\ndescriptors from other values).\n\n---\n\n### Step 16: Refinement Types\n\nA refinement type combines a base type with an additional predicate. The\nvalue must satisfy both.\n\n#### `make-refinement-type`\n\n```scheme\n(make-refinement-type 'integer positive?)\n; => refinement-type record: base=integer, pred=positive?\n```\n\n#### Accessors\n\n```scheme\n(refinement-type? x)          ; predicate\n(refinement-type-base rt)     ; => base type symbol\n(refinement-type-pred rt)     ; => predicate procedure or symbol\n```\n\n#### `check-refinement!`\n\n```scheme\n(check-refinement! who name val base pred-spec)\n```\n\nIn `debug` mode: first checks that `val` satisfies `base`, then calls\n`pred-spec` on `val`. Either check failure raises an error. `pred-spec` may\nbe a procedure or a symbol (resolved via `eval`).\n\n#### `assert-refined`\n\n```\n(assert-refined expr base-type pred-expr)\n```\n\nInline macro version. Evaluates `expr`, runs both checks, and returns the\nvalue.\n\n```scheme\n(define (set-port! [p : fixnum])\n  (assert-refined p integer positive?)   ; must be a positive integer\n  ...)\n\n;; Or with a lambda predicate\n(define (bounded-index! [i : fixnum])\n  (assert-refined i fixnum (lambda (n) (and (>= n 0) (< n max-index))))\n  ...)\n```\n\n---\n\n### Step 17: Type-Directed Compilation\n\n`define/tc` and `lambda/tc` extend `define/t` and `lambda/t` with\nautomatic arithmetic specialization based on declared types.\n\n**Specialization rules:**\n\n- All arguments _and_ return type are `fixnum` \u2192 body wrapped in\n  `(with-fixnum-ops ...)` automatically\n- All arguments _and_ return type are `flonum` \u2192 body wrapped in\n  `(with-flonum-ops ...)`\n- Mixed or other types \u2192 no specialization (generic ops, return type\n  checked normally)\n\n```\n(define/tc (name [arg : type] ...) : ret-type body ...)\n(lambda/tc ([arg : type] ...) : ret-type body ...)\n```\n\n```scheme\n;; All fixnum \u2192 automatic fx+ fx- fx* rewriting, zero overhead in release\n(define/tc (dot-product-int [a : fixnum] [b : fixnum] [c : fixnum] [d : fixnum]) : fixnum\n  (+ (* a c) (* b d)))\n\n;; All flonum \u2192 automatic fl+ fl* etc.\n(define/tc (hypot [x : flonum] [y : flonum]) : flonum\n  (sqrt (+ (* x x) (* y y))))\n\n;; Mixed \u2192 no rewriting, just type checks\n(define/tc (format-value [n : number] [label : string]) : string\n  (string-append label \": \" (number->string n)))\n```\n\n`define/tc` also handles `(Refine base pred)` annotations in argument\nposition:\n\n```scheme\n(define/tc (sqrt-positive [x : (Refine flonum flpositive?)]) : flonum\n  (flsqrt x))\n```\n\nIn `debug` mode this checks both that `x` is a `flonum` and that\n`(flpositive? x)` holds before executing the body.\n\n---\n\n### Union and Intersection type specs\n\n```\n(Union T1 T2 ...)\n(Intersection T1 T2 ...)\n```\n\nThese produce predicate procedures at runtime. They can be passed to\n`register-type-predicate!` to give the combined type a name.\n\n```scheme\n(register-type-predicate! 'number-or-string\n  (Union number string))\n\n(register-type-predicate! 'positive-real\n  (Intersection real (lambda (x) (positive? x))))\n\n(define/t (display-value [x : number-or-string]) : string\n  (if (number? x) (number->string x) x))\n```\n\n---\n\n## Complete Example\n\n```scheme\n(import (chezscheme)\n        (std typed)\n        (std typed advanced))\n\n;; ---- Custom type ----\n(register-type-predicate! 'non-empty-string\n  (lambda (s) (and (string? s) (> (string-length s) 0))))\n\n;; ---- Basic typed define ----\n(define/t (greet [name : non-empty-string]) : string\n  (string-append \"Hello, \" name \"!\"))\n\n;; ---- Type-directed fixnum arithmetic ----\n(define/tc (factorial [n : fixnum]) : fixnum\n  (if (fxzero? n) 1 (fx* n (factorial (fx1- n)))))\n\n;; ---- Occurrence typing ----\n(define (safe-length x)\n  (cond/t\n    [(list?   x) (length x)]\n    [(string? x) (string-length x)]\n    [(vector? x) (vector-length x)]\n    [else (error 'safe-length \"unsupported type\" x)]))\n\n;; ---- Row polymorphism ----\n(defrow Named\n  (name : string))\n\n(define (print-name obj)\n  (check-Named! obj)\n  (display (name obj)))\n\n;; ---- Refinement type ----\n(define (safe-sqrt [x : (Refine flonum flpositive?)])\n  (define/tc ([x : (Refine flonum flpositive?)]) : flonum\n    (flsqrt x)))\n\n;; ---- Effect annotation ----\n(define/te (read-line! [port : any]) : (Effect IO string)\n  (get-line port))\n\n;; ---- Switch to release for production ----\n;; (*typed-mode* 'release)  ; all checks become no-ops\n```\n\n---\n\n## Performance Notes\n\n| Scenario | Recommendation |\n|---|---|\n| Development / CI | Keep `(*typed-mode* 'debug)` (default). Annotations catch bugs early with descriptive messages. |\n| Production binary | Set `(*typed-mode* 'release)` at startup or in your build entry point. |\n| Hot inner loops with fixnum math | Use `define/tc` with full fixnum annotations; get `fx*` etc. for free. |\n| Hot inner loops with flonum math | Use `define/tc` with full flonum annotations; get `fl*`, `flsqrt`, etc. |\n| Annotating library boundaries only | Annotate public-facing functions; leave internal helpers untyped or use `assert-type` at key points. |\n| `(listof T)` on large lists | The predicate walks every element. Prefer `list?` if the annotation is just documentation. |\n\n`with-fixnum-ops` and `with-flonum-ops` are pure compile-time rewrites \u2014\nthey have identical performance to writing `fx+` / `fl+` by hand.\n\nThe effect of `(*typed-mode* 'release)` is runtime, not compile-time. If\nyou need truly zero-overhead release builds, wrap `define/t` forms in a\n`when` on a compile-time constant, or use `define/tc` which only emits\nchecks inside `(when (eq? (*typed-mode*) 'debug) ...)` guards that Chez can\noptimize away.\n"}
-{"text": "<!-- FILE: docs/jerboa-edge.md -->\n# Jerboa Edge: The Killer App\n\n**Goal:** A single-file, production-grade webhook processing service that\ndemonstrates why Jerboa exists \u2014 Clojure's data philosophy, Erlang's fault\ntolerance, Go's deployment story, and built-in security, all in one 15MB binary.\n\n**Status:** 2026-04-12 \u2014 Phase 4 complete\n\n---\n\n## Why This, Why Now\n\nJerboa's stdlib is deep enough.  580 modules, 4 concurrency models, persistent\ndata structures, cryptography, a Rust native backend.  What's missing is a\n**single artifact that makes someone say \"I need this.\"**\n\nLanguages don't win on feature lists.  They win on one undeniable demo:\n\n- Ruby had Rails (2004) \u2014 \"blog in 15 minutes\" video got 1M views\n- Go had Docker (2013) \u2014 proved single-binary deployment scales\n- Erlang had ejabberd \u2014 proved \"never go down\" wasn't marketing\n- Elixir had Phoenix LiveView \u2014 proved you don't need a JS framework\n\nJerboa needs its equivalent.  Not a framework \u2014 a **runnable, self-contained\nservice** that a Clojure developer can clone, read in 20 minutes, deploy in 60\nseconds, and immediately understand what they'd gain by switching.\n\n### Why a Webhook Service\n\nWebhook processing is the ideal demo because:\n\n1. **Everyone needs one.** Stripe, GitHub, Slack, Shopify, Twilio \u2014 every modern\n   SaaS pushes webhooks.  Every engineering team has a janky webhook handler.\n\n2. **It naturally requires everything Jerboa is good at:**\n   - High-concurrency HTTP ingestion (fiber-httpd)\n   - Fault-tolerant async processing (actors with supervision)\n   - Ordered message pipelines (CSP channels + transducers)\n   - Shared mutable state without locks (STM)\n   - Real-time monitoring (WebSocket dashboard)\n   - Sandboxed user-defined filters (Landlock)\n   - Zero-dependency deployment (static binary)\n\n3. **The Clojure comparison is devastating.**  In Clojure, you need: JVM + Redis\n   (for queuing) + Sidekiq/Celery (for processing) + nginx (for TLS/routing) +\n   Docker Compose to wire it all together.  4 processes, 2GB RAM, 30-second cold\n   starts.  In Jerboa, it's one file, one binary, 20MB of RAM, instant startup.\n\n4. **It's small enough to be readable** but complex enough to be credible.  Target:\n   ~380 lines of Scheme, not 3000.\n\n---\n\n## What It Demonstrates\n\n| Feature shown | Jerboa module | Why it matters |\n|---|---|---|\n| Accept 100K concurrent webhooks | `(std net fiber-httpd)` | Fibers, not threads \u2014 4KB each |\n| Route by path + method | `(std net router)` | Clean URL dispatch with params |\n| Parse/validate JSON payloads | `(std text json)` | Zero-dependency JSON |\n| Queue work without Redis | `(std csp clj)` | CSP channels are built in |\n| Transform payloads in flight | `(std transducer)` | Composable, no intermediate allocs |\n| Process with fault tolerance | `(std actor)` | Supervision trees restart crashes |\n| Coordinate shared state | `(std stm)` | STM refs, not mutexes |\n| Store in persistent collections | `(std pmap)`, `(std pvec)` | Immutable audit trail |\n| Stream live updates | `(std net fiber-ws)` | WebSocket on same server |\n| Sandbox user filters | `(std security sandbox)` | Landlock/seccomp per eval |\n| Manage lifecycle | `(std component)` | Clean start/stop ordering |\n| Deploy anywhere | `make static` | One musl binary, scp it |\n\n---\n\n## Architecture\n\n```\n                    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                    \u2502      fiber-httpd           \u2502\n                    \u2502   (one fiber per conn)     \u2502\n                    \u2502   epoll-backed accept      \u2502\n                    \u2514\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                          \u2502            \u2502\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u25bc                                    \u25bc\n     POST /hooks/:type                    GET /dashboard\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n     \u2502 1. Parse JSON    \u2502                 \u2502 WebSocket       \u2502\n     \u2502 2. Validate sig  \u2502                 \u2502 upgrade         \u2502\n     \u2502 3. Respond 202   \u2502                 \u2502 (fiber-ws)      \u2502\n     \u2502 4. Enqueue       \u2502                 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                         \u2502\n              \u2502                                   \u2502\n              \u25bc                                   \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u2502\n     \u2502 Ingest Channel           \u2502                 \u2502\n     \u2502 (std csp clj)            \u2502                 \u2502\n     \u2502                          \u2502                 \u2502\n     \u2502 Transducer pipeline:     \u2502                 \u2502\n     \u2502  validate \u2192 normalize    \u2502                 \u2502\n     \u2502  \u2192 deduplicate \u2192 route   \u2502                 \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                 \u2502\n              \u2502                                   \u2502\n              \u25bc                                   \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u2502\n     \u2502 Worker Pool              \u2502                 \u2502\n     \u2502 (actor supervisor)       \u2502                 \u2502\n     \u2502                          \u2502                 \u2502\n     \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510   \u2502                 \u2502\n     \u2502 \u2502worker-1\u2502 \u2502worker-2\u2502..N\u2502                 \u2502\n     \u2502 \u2514\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518   \u2502                 \u2502\n     \u2502     \u2502          \u2502        \u2502                 \u2502\n     \u2502  process    process     \u2502                 \u2502\n     \u2502  webhook    webhook     \u2502                 \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                 \u2502\n              \u2502                                  \u2502\n              \u25bc                                  \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                \u2502\n     \u2502 State Store              \u2502                \u2502\n     \u2502 (STM refs + pmap)        \u2502     updates    \u2502\n     \u2502                          \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n     \u2502 Immutable snapshots      \u2502\n     \u2502 of all webhook events    \u2502\n     \u2502 + processing results     \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n### Data Flow\n\n1. **Ingest:** fiber-httpd receives POST, one fiber handles the connection.\n   Handler parses JSON, validates HMAC signature, writes a `202 Accepted`\n   response immediately, then enqueues the event onto the ingest channel.  The\n   fiber is freed in microseconds \u2014 no blocking on processing.\n\n2. **Pipeline:** The ingest channel has a transducer stack attached:\n   - `(filtering valid-signature?)` \u2014 drop replayed/forged events\n   - `(mapping normalize-event)` \u2014 canonicalize field names, timestamps\n   - `(deduplicate)` \u2014 idempotency via event ID\n   - `(mapping route-event)` \u2014 tag with destination handler\n\n3. **Processing:** A supervisor manages N worker actors.  Each worker pulls from\n   the channel, executes the handler, writes results to the state store.  If a\n   worker crashes (bad payload, handler bug), the supervisor restarts it.  The\n   other workers continue unaffected.\n\n4. **State:** All webhook events and results live in STM-managed persistent maps.\n   `(dosync (alter events conj new-event))`.  No locks, no race conditions.\n   Every state transition is an immutable snapshot \u2014 you get a full audit trail\n   for free.\n\n5. **Dashboard:** A WebSocket endpoint streams state changes to connected\n   browsers.  Each WS connection is a fiber.  When the STM refs change, watchers\n   push updates to all connected dashboard fibers.\n\n6. **Sandboxing (optional):** Users can define filter expressions that run in a\n   Landlock sandbox.  `(run-safe-eval user-filter-code config)` executes in a\n   forked child with syscall restrictions.  The parent process is never at risk.\n\n---\n\n## API Design\n\n### Starting the Service\n\n```scheme\n#!/usr/bin/env scheme --libdirs lib --script\n(import (jerboa prelude)\n        (std clojure)\n        (std component)\n        (std net fiber-httpd)\n        (std net fiber-ws)\n        (std net router)\n        (std csp clj)\n        (std actor)\n        (std stm)\n        (std transducer)\n        (std text json)\n        (std crypto hmac))\n\n(def (main)\n  (let ([sys (-> (system-map\n                   'config    (component 'config)\n                   'state     (component 'state)\n                   'ingest    (component 'ingest)\n                   'workers   (component 'workers)\n                   'http      (component 'http))\n                 (system-using\n                   '((state   . (config))\n                     (ingest  . (config state))\n                     (workers . (config state ingest))\n                     (http    . (config state ingest)))))])\n    (displayln \"[edge] starting on port 8080...\")\n    (start sys)))\n\n(main)\n```\n\n### Webhook Ingestion Endpoint\n\n```scheme\n(def (handle-webhook req)\n  ;; 1. Parse the JSON body\n  (let* ([body     (request-body req)]\n         [payload  (string->json-object body)]\n         [type     (route-param req \"type\")]\n         [sig      (request-header req \"x-signature\")])\n\n    ;; 2. Verify HMAC signature\n    (unless (verify-hmac-sha256 secret sig body)\n      (respond-json 401 \"{\\\"error\\\":\\\"invalid signature\\\"}\"))\n\n    ;; 3. Build internal event record\n    (let ([event (hash-map\n                   \"id\"         (hash-ref payload \"id\" (uuid))\n                   \"type\"       type\n                   \"payload\"    payload\n                   \"received\"   (datetime->iso8601 (datetime-now))\n                   \"status\"     \"queued\")])\n\n      ;; 4. Enqueue for async processing \u2014 this returns immediately\n      (>!! ingest-ch event)\n\n      ;; 5. Respond 202 before processing starts\n      (respond-json 202\n        (json-object->string\n          (hash-map \"status\" \"queued\"\n                    \"id\"     (hash-ref event \"id\")))))))\n```\n\n### Transducer Pipeline\n\n```scheme\n(def ingest-xf\n  (compose-transducers\n    ;; Drop events with duplicate IDs (idempotency)\n    (deduplicate)\n\n    ;; Normalize: ensure all events have a timestamp\n    (mapping\n      (lambda (evt)\n        (if (hash-key? evt \"timestamp\")\n          evt\n          (let ([ht (make-hash-table)])\n            (hash-for-each (lambda (k v) (hash-put! ht k v)) evt)\n            (hash-put! ht \"timestamp\" (hash-ref evt \"received\"))\n            ht))))\n\n    ;; Drop obviously malformed events\n    (filtering\n      (lambda (evt)\n        (and (hash-key? evt \"type\")\n             (hash-key? evt \"payload\"))))))\n\n;; Create the ingest channel with the transducer attached\n(def ingest-ch (chan 4096 ingest-xf))\n```\n\n### Supervised Worker Pool\n\n```scheme\n(def (make-worker-behavior state-ref ingest-ch handlers)\n  ;; Each worker is an actor that loops reading from the channel\n  (lambda (msg)\n    (match msg\n      ['start\n       ;; Enter processing loop\n       (let loop ()\n         (let ([event (<!! ingest-ch)])\n           (when event\n             ;; Look up the handler for this event type\n             (let* ([type    (hash-ref event \"type\" \"unknown\")]\n                    [handler (hash-ref handlers type default-handler)]\n                    [result  (try\n                               (ok (handler event))\n                               (catch (e)\n                                 (err (error-message e))))])\n\n               ;; Record result in STM state\n               (dosync\n                 (alter state-ref\n                   (lambda (st)\n                     (let ([id (hash-ref event \"id\")])\n                       (hash-put! st id\n                         (hash-map\n                           \"event\"     event\n                           \"result\"    result\n                           \"processed\" (datetime->iso8601\n                                         (datetime-now))))\n                       st))))\n\n               ;; Notify dashboard watchers\n               (notify-watchers! event result))\n\n             (loop))))]\n\n      ;; Graceful shutdown\n      ['stop (void)])))\n\n(def (start-worker-pool n state-ref ingest-ch handlers)\n  (let ([children\n         (map (lambda (i)\n                (make-child-spec\n                  (string->symbol (format \"worker-~a\" i))\n                  (lambda ()\n                    (let ([ref (spawn-actor\n                                 (make-worker-behavior\n                                   state-ref ingest-ch handlers)\n                                 (format \"worker-~a\" i))])\n                      (send ref 'start)\n                      ref))\n                  'permanent     ;; always restart on crash\n                  5              ;; 5s shutdown grace\n                  'worker))\n              (iota n))])\n    (start-supervisor 'one-for-one children 10 60)))\n```\n\n### STM State Store\n\n```scheme\n;; The entire application state is one STM ref holding a persistent map.\n;; Every mutation is atomic, consistent, and produces an immutable snapshot.\n\n(def state-ref (make-ref (make-hash-table)))\n\n;; Read current state (outside transaction \u2014 snapshot)\n(def (get-event id)\n  (hash-get (ref-deref state-ref) id))\n\n;; Read all events for a type\n(def (get-events-by-type type)\n  (let ([st (ref-deref state-ref)])\n    (filter\n      (lambda (entry)\n        (let ([evt (hash-ref (cdr entry) \"event\")])\n          (equal? (hash-ref evt \"type\") type)))\n      (hash->list st))))\n\n;; Stats: count by status\n(def (event-stats)\n  (let ([st (ref-deref state-ref)])\n    (let ([total   0]\n          [ok-n    0]\n          [err-n   0])\n      (hash-for-each\n        (lambda (id record)\n          (set! total (+ total 1))\n          (let ([r (hash-ref record \"result\")])\n            (if (ok? r)\n              (set! ok-n (+ ok-n 1))\n              (set! err-n (+ err-n 1)))))\n        st)\n      (hash-map \"total\" total\n                \"ok\"    ok-n\n                \"error\" err-n))))\n```\n\n### WebSocket Dashboard\n\n```scheme\n;; Connected dashboard clients \u2014 a simple list protected by STM\n(def watchers-ref (make-ref '()))\n\n(def (handle-dashboard req fd poller)\n  ;; Upgrade HTTP \u2192 WebSocket\n  (let ([ws (fiber-ws-upgrade (request-headers req) fd poller)])\n    (when ws\n      ;; Register this connection\n      (dosync (alter watchers-ref (lambda (ws-list) (cons ws ws-list))))\n\n      ;; Send current state snapshot\n      (fiber-ws-send ws\n        (json-object->string (event-stats)))\n\n      ;; Keep alive \u2014 recv loop handles pings and detects disconnect\n      (let loop ()\n        (let ([msg (fiber-ws-recv ws)])\n          (cond\n            [(not msg)\n             ;; Client disconnected \u2014 unregister\n             (dosync\n               (alter watchers-ref\n                 (lambda (ws-list)\n                   (filter (lambda (w) (not (eq? w ws))) ws-list))))]\n            [else (loop)]))))))\n\n;; Called by workers after processing an event\n(def (notify-watchers! event result)\n  (let ([update (json-object->string\n                  (hash-map\n                    \"type\"   \"event-processed\"\n                    \"id\"     (hash-ref event \"id\")\n                    \"status\" (if (ok? result) \"ok\" \"error\")\n                    \"time\"   (datetime->iso8601 (datetime-now))))])\n    (for-each\n      (lambda (ws)\n        (guard (_ [#t (void)])  ;; ignore send errors on dead connections\n          (fiber-ws-send ws update)))\n      (ref-deref watchers-ref))))\n```\n\n### Sandboxed User Filters\n\n```scheme\n;; Users can submit filter expressions that run in a Landlock sandbox.\n;; The filter receives the event as a hash table binding `evt` and must\n;; return #t (accept) or #f (drop).\n\n(def (eval-user-filter filter-code event)\n  (let ([code (format\n                \"(let ([evt '~s]) ~a)\"\n                (hash->list event)\n                filter-code)]\n        [config (make-sandbox-config\n                  'timeout 2           ;; 2 second hard limit\n                  'seccomp 'pure-computation  ;; no I/O, no network\n                  'max-output-size 1024)])\n    (run-safe-eval code config)))\n\n;; Example usage:\n;; User submits: (equal? (cdr (assoc \"type\" evt)) \"payment.completed\")\n;; This runs in a forked child with:\n;;   - No filesystem access (Landlock)\n;;   - No network syscalls (seccomp)\n;;   - 2 second timeout\n;;   - Parent process is never at risk\n```\n\n### HTTP Router Assembly\n\n```scheme\n(def (make-edge-router state-ref ingest-ch)\n  (let ([r (make-router)])\n\n    ;; Webhook ingestion\n    (router-post! r \"/hooks/:type\"\n      (lambda (req) (handle-webhook req)))\n\n    ;; Query API\n    (router-get! r \"/api/events/:id\"\n      (lambda (req)\n        (let ([id (route-param req \"id\")])\n          (aif (get-event id)\n            (respond-json 200 (json-object->string it))\n            (respond-json 404 \"{\\\"error\\\":\\\"not found\\\"}\")))))\n\n    (router-get! r \"/api/stats\"\n      (lambda (req)\n        (respond-json 200\n          (json-object->string (event-stats)))))\n\n    ;; Dashboard WebSocket\n    (router-get! r \"/dashboard\"\n      (lambda (req)\n        (make-websocket-response\n          (lambda (fd poller req)\n            (handle-dashboard req fd poller)))))\n\n    ;; Health check\n    (router-get! r \"/health\"\n      (lambda (req)\n        (respond-json 200 \"{\\\"status\\\":\\\"ok\\\"}\")))\n\n    r))\n```\n\n### Full Startup\n\n```scheme\n(def (run-edge port num-workers)\n  (let* ([state-ref   (make-ref (make-hash-table))]\n         [ingest-ch   (chan 4096 ingest-xf)]\n         [handlers    (make-hash-table)]\n         [router      (make-edge-router state-ref ingest-ch)])\n\n    ;; Register webhook handlers by type\n    (hash-put! handlers \"payment.completed\"\n      (lambda (evt)\n        (displayln \"[payment] processing \" (hash-ref evt \"id\"))))\n\n    (hash-put! handlers \"user.created\"\n      (lambda (evt)\n        (displayln \"[user] processing \" (hash-ref evt \"id\"))))\n\n    ;; Start supervised worker pool\n    (let ([supervisor\n           (start-worker-pool num-workers state-ref ingest-ch handlers)])\n\n      ;; Start HTTP server on fibers\n      (displayln \"[edge] listening on :\" port)\n      (displayln \"[edge] \" num-workers \" workers supervised\")\n      (displayln \"[edge] dashboard at ws://localhost:\" port \"/dashboard\")\n\n      (fiber-httpd-start port\n        (lambda (req)\n          (let ([match (router-match router\n                         (request-method req)\n                         (request-path-only req))])\n            (if match\n              ((route-match-handler match) req)\n              (respond-json 404 \"{\\\"error\\\":\\\"not found\\\"}\"))))))))\n\n;; Entry point\n(run-edge 8080 4)\n```\n\n---\n\n## The Comparison That Sells It\n\n### Clojure Equivalent\n\nTo build the same webhook processor in Clojure, you need:\n\n```\nproject.clj (or deps.edn)          \u2014 dependency management\n\u251c\u2500\u2500 JVM 17+                        \u2014 200MB runtime\n\u251c\u2500\u2500 ring/ring-core                 \u2014 HTTP abstraction\n\u251c\u2500\u2500 http-kit or aleph              \u2014 async HTTP server\n\u251c\u2500\u2500 compojure or reitit            \u2014 routing\n\u251c\u2500\u2500 cheshire                       \u2014 JSON parsing\n\u251c\u2500\u2500 core.async                     \u2014 channels\n\u251c\u2500\u2500 mount or component             \u2014 lifecycle\n\u251c\u2500\u2500 redis (Carmine)                \u2014 job queue\n\u251c\u2500\u2500 sidekiq or machinery           \u2014 background workers\n\u251c\u2500\u2500 timbre                         \u2014 logging\n\u2514\u2500\u2500 Docker + docker-compose.yml    \u2014 deployment\n```\n\n**Result:**\n- 12+ dependencies (each with transitive deps \u2192 100+ JARs)\n- 3 processes minimum (JVM app + Redis + worker process)\n- 30-second cold start (JVM + dependency loading)\n- 512MB-2GB RAM (JVM heap + Redis)\n- Docker image: 400MB-1GB\n- `docker-compose up` to run locally\n- 5-10 files of boilerplate before writing business logic\n\n### Jerboa Equivalent\n\n```\nwebhook-service.ss                  \u2014 the entire service\n```\n\n**Result:**\n- 0 external dependencies (everything is stdlib)\n- 1 process (one binary does HTTP + workers + state + WebSocket)\n- Instant startup (Chez native code, no interpreter warmup)\n- 20-40MB RAM (no GC pressure from boxed JVM objects)\n- Static binary: 15MB\n- `scp edge user@server: && ssh server ./edge` to deploy\n- ~380 lines, one file, zero config files\n\n### Side-by-Side: Processing a Webhook\n\n**Clojure:**\n```clojure\n;; deps: ring, compojure, cheshire, core.async, carmine (redis)\n\n(ns webhook.handler\n  (:require [compojure.core :refer [defroutes POST]]\n            [ring.middleware.json :refer [wrap-json-body]]\n            [cheshire.core :as json]\n            [clojure.core.async :as async]\n            [taoensso.carmine :as redis]))\n\n(defroutes app-routes\n  (POST \"/hooks/:type\" [type :as req]\n    (let [payload (:body req)]\n      ;; Push to Redis queue (external process!)\n      (redis/wcar {} (redis/lpush \"webhooks\" (json/encode payload)))\n      {:status 202\n       :body (json/encode {:status \"queued\"})})))\n\n;; Separate worker process reads from Redis:\n(defn worker []\n  (loop []\n    (let [raw (redis/wcar {} (redis/brpop \"webhooks\" 0))\n          event (json/decode (second raw) true)]\n      (process-event! event)\n      (recur))))\n```\n\n**Jerboa:**\n```scheme\n;; deps: none (everything is stdlib)\n\n(import (jerboa prelude)\n        (std clojure)\n        (std net fiber-httpd)\n        (std csp clj)\n        (std text json))\n\n(def work-ch (chan 4096))\n\n(def (handle-webhook req)\n  (let ([payload (string->json-object (request-body req))])\n    (>!! work-ch payload)\n    (respond-json 202 \"{\\\"status\\\":\\\"queued\\\"}\")))\n\n;; Workers are fibers on the same process \u2014 no Redis, no IPC\n(dotimes (i 4)\n  (go (let loop ()\n        (let ([event (<!! work-ch)])\n          (when event\n            (process-event! event)\n            (loop))))))\n\n(fiber-httpd-start 8080 handle-webhook)\n```\n\nThe Jerboa version is **12 lines** vs Clojure's **20+ lines + Redis + a separate\nworker process**.  And the Jerboa version handles more concurrent connections\n(fibers vs threads), has built-in fault tolerance (supervisor), and deploys as\none file.\n\n---\n\n## Implementation Plan\n\n### Phase 1: Core Demo (the ~380-line file) \u2014 COMPLETE\n\n**Deliverable:** [`jerboa-edge/edge.ss`](https://github.com/ober/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://github.com/ober/jerboa-edge\ncd jerboa-edge\nmake run\n\n# In another terminal:\ncurl -X POST localhost:8080/hooks/payment.completed \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"evt_123\",\"amount\":4999}'\n\ncurl localhost:8080/api/stats\n# => {\"total\":1,\"ok\":1,\"error\":0}\n```\n\n**Scope:**\n\n- [x] 1.1 **HTTP server with routing** \u2014 fiber-httpd with `:param` pattern routing.\n    Five endpoints: `POST /hooks/:type`, `GET /api/events/:id`, `GET /api/stats`,\n    `GET /health`, `GET /dashboard`.\n\n- [x] 1.2 **Channel-based ingestion** \u2014 CSP channel with transducer pipeline\n    (validate + dedup-by-ID + normalize).  Decouples accept from process.\n\n- [x] 1.3 **Actor-supervised workers** \u2014 N worker actors under a one-for-one\n    supervisor.  Each pulls from the channel, processes, writes to state.\n    `POST /hooks/test.crash` deliberately crashes a worker to show supervisor restart.\n\n- [x] 1.4 **STM state store** \u2014 All state in `(make-ref (make-persistent-map))`.\n    Atomic updates via `dosync`/`alter` with `(std pmap)` persistent maps.\n    Immutable snapshots \u2014 safe for concurrent reads during writes.\n\n- [x] 1.5 **WebSocket dashboard** \u2014 Single WS endpoint that streams\n    event-processed notifications to connected clients.  Sends initial stats\n    on connect.\n\n- [x] 1.6 **HMAC signature verification** \u2014 Validate `X-Signature` header using\n    `(std crypto native)` HMAC-SHA256.  Constant-time comparison.\n    Configurable via `EDGE_SECRET` env var.\n\n**Non-goals for Phase 1:**\n- Persistence across restarts (in-memory only)\n- TLS termination (use a reverse proxy, or wait for Phase 3)\n- Static binary packaging (Phase 3)\n\n**Success criteria:**\n- [x] Runs with `make run` (382 lines of Scheme)\n- [x] Handles 10K concurrent webhook POSTs without dropped events (verified: 9,993/10,000 accepted at 200 concurrent connections, ~454 req/sec)\n- [x] Worker crash + supervisor restart visible in logs\n- [x] WebSocket dashboard receives live event stream (verified with websocket-client)\n- [x] `curl` examples work out of the box (17/17 smoke tests pass)\n- [x] HMAC: no-sig and wrong-sig return 401, valid-sig returns 202\n\n### Phase 2: Programmability\n\n**Deliverable:** Sandboxed user filter support + handler registry\n\n- [x] 2.1 **Sandboxed eval** \u2014 Users submit filter expressions via\n    `POST /api/filters`.  Filters evaluated per-event via\n    `restricted-eval-string` (allowlist-only: no I/O, no FFI) with a\n    2-second engine-based timeout (no fork \u2014 fiber-safe).  Events that\n    fail any filter are dropped silently before processing.\n\n    ```\n    POST /api/filters  {\"name\":\"payments-only\",\"code\":\"(equal? (cdr (assoc \\\"type\\\" evt)) \\\"payment.completed\\\")\"}\n    GET  /api/filters\n    DELETE /api/filters/:name\n    ```\n\n    Key implementation detail: event data is bound as `evt` (a deep\n    alist), so filter code uses `assoc` with string keys (not `assq`).\n\n- [x] 2.2 **Hot handler registration** \u2014 New webhook types registered at\n    runtime via `POST /api/handlers`.  No restart required.  Handler\n    code runs in a custom restricted environment (`safe-bindings` +\n    `displayln`) with a 5-second engine timeout.  Exceptions propagate\n    to the worker's retry machinery.\n\n    ```\n    POST /api/handlers  {\"type\":\"order.shipped\",\"code\":\"(displayln \\\"shipped: \\\" (cdr (assoc \\\"id\\\" evt)))\"}\n    GET  /api/handlers\n    ```\n\n- [x] 2.3 **Retry with exponential backoff** \u2014 Failed events re-enter the\n    channel with an incremented retry count.  Workers delay via\n    `fork-thread` + `sleep` (non-blocking, OS-thread safe).  Dedup\n    transducer passes retried events (`retry-count > 0`).  After 3\n    retries, events go to a dead-letter STM store.\n\n    ```\n    GET  /api/dead-letter\n    POST /api/dead-letter/:id/replay\n    ```\n\n    Replay sets `retry-count: 1` to bypass the dedup filter.\n\n**Success criteria:**\n- [x] Filter code cannot call `open-file`, `socket`, or any I/O (allowlist env)\n- [x] Filter with 2s timeout verified: `with-timeout` uses Chez engines, no fork\n- [x] Hot-registered handlers process events without service restart (verified: `order.shipped`, `inventory.updated`)\n- [x] Failed events retry 3\u00d7 with 1s/2s/4s backoff then dead-letter (verified in log)\n- [x] Dead-letter replay re-processes event and clears queue (verified: status \u2192 \"ok\")\n- [x] Runs with `make run` (690 lines of Scheme)\n\n### Phase 3: Production Hardening \u2713\n\n**Deliverable:** Static binary, TLS, persistence, metrics (~1048 lines)\n\n- [x] 3.1 **Static musl binary** \u2014 `make edge-static` produces a fully static\n    ELF via `docker run jerboa21/jerboa` (musl Chez + Rust toolchain + WPO).\n    **Verified: 12MB, zero runtime dependencies.**  All native FFI symbols\n    registered via `Sforeign_symbol`; `JERBOA_STATIC=1` auto-set at startup\n    to bypass `load-shared-object` calls.  Deploy with `scp edge host:/usr/local/bin/`.\n\n- [x] 3.2 **TLS termination** \u2014 Optional `EDGE_TLS_CERT` / `EDGE_TLS_KEY`\n    env vars for direct HTTPS.  Uses `(std net tls-rustls)` \u2014 the Rust\n    rustls backend.  `make run-tls` target.  One OS thread per TLS connection;\n    same router handler for both plain and TLS paths.\n\n- [x] 3.3 **SQLite persistence** \u2014 `EDGE_DB_PATH` env var enables persistence\n    via `(std db sqlite-native)` (bundled rusqlite, no external libsqlite3).\n    WAL mode for concurrent reads.  Events and dead-letters loaded into STM\n    on startup.  All writes are fire-and-forget (guarded; SQLite error never\n    crashes a worker).  `make run-db` target.\n\n- [x] 3.4 **Prometheus metrics** \u2014 `/metrics` endpoint in Prometheus text\n    format via `(std metrics)`.  Exposed counters and histogram:\n    - `edge_events_received_total`\n    - `edge_events_ok_total`\n    - `edge_events_error_total`\n    - `edge_events_dead_letter_total`\n    - `edge_worker_restarts_total`\n    - `edge_processing_duration_seconds` (histogram, default buckets)\n    - `edge_active_connections` (gauge)\n\n- [x] 3.5 **Structured JSON logging** \u2014 All event-path log lines are JSON\n    (`jlog` helper, `json-object->string`).  Fields: `ts`, `msg`, plus\n    context fields (`worker`, `id`, `type`, `status`, `ms`, `err`).\n    Startup banner stays human-readable.\n\n- [x] 3.6 **Graceful shutdown** \u2014 `SIGTERM` triggers via `add-signal-handler!`\n    from `(std os signal)`.  Sequence: `fiber-httpd-stop!` \u2192 `chan-close!`\n    (workers see EOF) \u2192 3s drain window \u2192 `PRAGMA wal_checkpoint(FULL)` \u2192\n    `(exit 0)`.  Verified: completes in ~3s.\n\n**Success criteria:**\n- [x] `make edge-static` builds and runs \u2014 12MB, fully static, verified functional\n- [x] Graceful shutdown completes in ~3 seconds (verified)\n- [x] Prometheus scrape at `/metrics` returns well-formed text format (verified)\n- [x] SQLite events survive restart: `total:2 ok:2` after reload (verified)\n- [x] JSON log lines include worker, event id, type, status, duration (verified)\n- [x] Runs with `make run` (~1048 lines of Scheme)\n\n### Phase 4: Distribution \u2713\n\n**Deliverable:** Multi-node webhook processing via authenticated TCP transport and in-process Raft (~1244 lines)\n\n- [x] 4.1 **Clustered actors via `(std actor transport)`** \u2014 When\n    `EDGE_CLUSTER_COOKIE` is set, each node registers itself with\n    `start-node!` and starts a TCP server with `start-node-server!`.\n    Transport uses HMAC-SHA256 4-step challenge-response authentication\n    with per-message integrity (derived session key).  Each node spawns\n    a named `'edge-remote-ingest` actor that receives `(work event)`\n    messages from peers and enqueues them into the local ingest channel.\n    After HTTP starts, `connect-to-peers!` calls `GET /api/cluster/info`\n    on each peer to discover actor IDs, then builds remote refs via\n    `make-remote-actor-ref`.  Incoming webhooks are round-robined across\n    self + connected peers; failed peer sends fall back to local.\n    Wire format: `[4-byte BE length][fasl body][32-byte HMAC-SHA256]`.\n\n    ```bash\n    # Terminal 1 \u2014 node A (HTTP :8080, transport :9000)\n    make run-cluster-a\n\n    # Terminal 2 \u2014 node B (HTTP :8081, transport :9001)\n    make run-cluster-b\n\n    # POST to either node; events processed on either worker pool\n    curl -X POST localhost:8080/hooks/payment.completed -d '{\"id\":\"e1\",...}'\n    curl localhost:8080/api/cluster/info   # shows node_id, actor_id, peers\n    ```\n\n- [x] 4.2 **Replicated event ordering via `(std raft)`** \u2014 When\n    `EDGE_RAFT_SIZE > 0`, an N-node in-process Raft cluster runs alongside\n    the service.  Every accepted webhook is proposed to the Raft commit\n    log by the current leader (`raft-propose!`), providing a consensus-\n    ordered event sequence.  The commit log accumulates in-memory and is\n    available via `raft-log`/`raft-commit-index` on the leader node.\n\n    Note: `(std raft)` uses in-memory channels (no TCP) \u2014 it is an\n    in-process simulation designed to be bridged over a transport layer\n    for true cross-process Raft.  `(std crdt)` does not exist in the\n    stdlib; per-node STM with coordinator routing is used instead.\n\n    Note: `(std raft)` uses in-memory channels \u2014 cross-process Raft\n    would require bridging node inboxes over `(std actor transport)`.\n    `(std crdt)` does not exist in the stdlib.\n\n    ```bash\n    make run-raft RAFT_SIZE=3\n    curl localhost:8080/api/cluster/info\n    # => {\"raft_nodes\":3,\"raft_leader\":\"yes\",\"raft_term\":3,\"raft_commit\":5,...}\n    ```\n\n- [x] 4.3 **`GET /api/cluster/info`** \u2014 New endpoint returns full cluster\n    state: `node_id`, `ingest_actor_id`, `cluster_enabled`, `peers_connected`,\n    `raft_nodes`, `raft_leader`, `raft_term`, `raft_commit`.  Used by\n    peers at startup to discover actor IDs for remote refs.\n\n- [x] 4.4 **Transport shutdown** \u2014 `graceful-shutdown!` calls\n    `transport-shutdown!` (closes all authenticated TCP connections) and\n    `raft-stop!` on each Raft node before exit.\n\n**Success criteria:**\n- [x] `EDGE_CLUSTER_COOKIE=x make run` starts transport server on port 9000\n- [x] `make run-raft RAFT_SIZE=3` shows \"leader: elected, term: 3\" in logs\n- [x] `/api/cluster/info` returns raft_leader, raft_term, raft_commit (verified)\n- [x] Webhook processed correctly with Raft enabled (verified: status \"ok\")\n- [x] Graceful shutdown closes transport + stops Raft nodes\n- [x] Runs with `make run` (~1244 lines of Scheme)\n\n---\n\n## Module Dependency Map\n\nOnly stdlib modules \u2014 zero external dependencies:\n\n```\nexamples/webhook-service.ss\n\u251c\u2500\u2500 (jerboa prelude)              ;; base language\n\u251c\u2500\u2500 (std clojure)                 ;; polymorphic ops, inc/dec, etc.\n\u251c\u2500\u2500 (std component)               ;; lifecycle management\n\u251c\u2500\u2500 (std net fiber-httpd)         ;; HTTP server (fiber-native)\n\u2502   \u2514\u2500\u2500 (std net io)              ;; epoll-backed fiber I/O\n\u251c\u2500\u2500 (std net fiber-ws)            ;; WebSocket (fiber-native)\n\u251c\u2500\u2500 (std net router)              ;; URL routing with params\n\u251c\u2500\u2500 (std csp clj)                 ;; Clojure-style channels\n\u2502   \u251c\u2500\u2500 (std csp)                 ;; base CSP\n\u2502   \u251c\u2500\u2500 (std csp select)          ;; alts!/timeout\n\u2502   \u251c\u2500\u2500 (std csp fiber-chan)      ;; fiber-aware channels\n\u2502   \u2514\u2500\u2500 (std fiber)               ;; M:N fiber runtime\n\u251c\u2500\u2500 (std actor)                   ;; actors + supervision\n\u251c\u2500\u2500 (std stm)                     ;; transactional memory\n\u251c\u2500\u2500 (std transducer)              ;; composable transforms\n\u251c\u2500\u2500 (std text json)               ;; JSON parser/writer\n\u251c\u2500\u2500 (std crypto hmac)             ;; HMAC-SHA256 verification\n\u251c\u2500\u2500 (std security sandbox)        ;; Landlock/seccomp eval\n\u2514\u2500\u2500 (std pmap)                    ;; persistent hash maps\n```\n\nAll of these ship with Jerboa.  `(std crypto hmac)` and\n`(std security sandbox)` use the Rust native backend; everything else is\npure Scheme.\n\n---\n\n## Benchmarking Targets\n\nThese numbers establish what \"production-grade\" means for the demo:\n\n| Metric | Target | How to measure |\n|---|---|---|\n| Webhook throughput | 50K events/sec sustained | `wrk -c 1000 -t 8 -d 30s` |\n| p99 accept latency | < 5ms | Measure time from connect to 202 |\n| Memory (100K queued) | < 100MB | `/proc/self/status` VmRSS |\n| Worker restart time | < 50ms | Log timestamp delta after kill |\n| WebSocket fan-out | 1000 clients, < 10ms broadcast | Measure from STM commit to WS send |\n| Cold start | < 100ms | Time from exec to first request served |\n| Binary size | < 25MB | `ls -la edge` |\n| Concurrent connections | 100K+ | `ulimit -n` + `wrk` |\n\n### Benchmark Script\n\n```bash\n#!/bin/bash\n# bench-edge.sh \u2014 run against a live edge instance\n\necho \"=== Throughput ===\"\nwrk -c 500 -t 4 -d 10s -s post.lua http://localhost:8080/hooks/test\n\necho \"=== Latency ===\"\nwrk -c 1 -t 1 -d 5s -s post.lua --latency http://localhost:8080/hooks/test\n\necho \"=== Memory ===\"\nps -o rss= -p $(pgrep -f webhook-service) | awk '{print $1/1024 \"MB\"}'\n\necho \"=== Stats ===\"\ncurl -s localhost:8080/api/stats | python3 -m json.tool\n```\n\n---\n\n## What This Proves\n\nWhen someone sees this demo and asks \"why not just use Clojure?\", the answer\nis concrete:\n\n1. **Deploy in 5 seconds, not 5 minutes.** One binary, scp, run.  No JVM, no\n   Docker, no Redis, no config files.\n\n2. **20MB of RAM, not 2GB.**  Fibers are 4KB each.  Persistent maps share\n   structure.  No JVM heap overhead, no Redis process.\n\n3. **100ms cold start, not 30 seconds.**  Chez compiles to native code.  No\n   classloading, no dependency injection framework initialization.\n\n4. **Fault tolerance is built in, not bolted on.**  Actor supervision trees\n   restart crashed workers.  STM prevents data races.  You don't need to\n   add \"resilience4j\" as a dependency.\n\n5. **Security is built in, not bolted on.**  Sandboxed eval uses kernel-level\n   isolation (Landlock/seccomp).  No \"eval is dangerous\" disclaimer \u2014 eval\n   is sandboxed by default.\n\n6. **One file, ~1244 lines.**  Phase 1 was ~380 lines.  Phase 4 adds TLS,\n   Prometheus metrics, SQLite persistence, graceful shutdown, distributed\n   actor transport with HMAC-SHA256 auth, and in-process Raft consensus \u2014\n   and the result is still one file a senior engineer can read in an\n   afternoon.  Try that with a Spring Boot webhook processor.\n\nThis isn't a toy.  It handles 50K events/sec, restarts crashed workers in\n50ms, streams live updates over WebSocket, scrapes cleanly into Prometheus,\nand the entire thing fits in a single file with zero external dependencies.\n\n---\n\n## Related Work\n\n- `docs/green-wins.md` \u2014 fiber-httpd production hardening roadmap\n- `docs/clojure-left.md` \u2014 Clojure gap analysis and priority roadmap\n- `docs/clojure-vs-jerboa.md` \u2014 feature parity comparison\n- `examples/hello-api.ss` \u2014 simple HTTP API example (starting point)\n- `examples/chat-server.ss` \u2014 WebSocket + concurrency example\n- `lib/std/component.sls` \u2014 Stuart Sierra lifecycle (used in Phase 1)\n"}
+{"text": "<!-- FILE: docs/jerboa-edge.md -->\n# Jerboa Edge: The Killer App\n\n**Goal:** A single-file, production-grade webhook processing service that\ndemonstrates why Jerboa exists \u2014 Clojure's data philosophy, Erlang's fault\ntolerance, Go's deployment story, and built-in security, all in one 15MB binary.\n\n**Status:** 2026-04-12 \u2014 Phase 4 complete\n\n---\n\n## Why This, Why Now\n\nJerboa's stdlib is deep enough.  580 modules, 4 concurrency models, persistent\ndata structures, cryptography, a Rust native backend.  What's missing is a\n**single artifact that makes someone say \"I need this.\"**\n\nLanguages don't win on feature lists.  They win on one undeniable demo:\n\n- Ruby had Rails (2004) \u2014 \"blog in 15 minutes\" video got 1M views\n- Go had Docker (2013) \u2014 proved single-binary deployment scales\n- Erlang had ejabberd \u2014 proved \"never go down\" wasn't marketing\n- Elixir had Phoenix LiveView \u2014 proved you don't need a JS framework\n\nJerboa needs its equivalent.  Not a framework \u2014 a **runnable, self-contained\nservice** that a Clojure developer can clone, read in 20 minutes, deploy in 60\nseconds, and immediately understand what they'd gain by switching.\n\n### Why a Webhook Service\n\nWebhook processing is the ideal demo because:\n\n1. **Everyone needs one.** Stripe, GitHub, Slack, Shopify, Twilio \u2014 every modern\n   SaaS pushes webhooks.  Every engineering team has a janky webhook handler.\n\n2. **It naturally requires everything Jerboa is good at:**\n   - High-concurrency HTTP ingestion (fiber-httpd)\n   - Fault-tolerant async processing (actors with supervision)\n   - Ordered message pipelines (CSP channels + transducers)\n   - Shared mutable state without locks (STM)\n   - Real-time monitoring (WebSocket dashboard)\n   - Sandboxed user-defined filters (Landlock)\n   - Zero-dependency deployment (static binary)\n\n3. **The Clojure comparison is devastating.**  In Clojure, you need: JVM + Redis\n   (for queuing) + Sidekiq/Celery (for processing) + nginx (for TLS/routing) +\n   Docker Compose to wire it all together.  4 processes, 2GB RAM, 30-second cold\n   starts.  In Jerboa, it's one file, one binary, 20MB of RAM, instant startup.\n\n4. **It's small enough to be readable** but complex enough to be credible.  Target:\n   ~380 lines of Scheme, not 3000.\n\n---\n\n## What It Demonstrates\n\n| Feature shown | Jerboa module | Why it matters |\n|---|---|---|\n| Accept 100K concurrent webhooks | `(std net fiber-httpd)` | Fibers, not threads \u2014 4KB each |\n| Route by path + method | `(std net router)` | Clean URL dispatch with params |\n| Parse/validate JSON payloads | `(std text json)` | Zero-dependency JSON |\n| Queue work without Redis | `(std csp clj)` | CSP channels are built in |\n| Transform payloads in flight | `(std transducer)` | Composable, no intermediate allocs |\n| Process with fault tolerance | `(std actor)` | Supervision trees restart crashes |\n| Coordinate shared state | `(std stm)` | STM refs, not mutexes |\n| Store in persistent collections | `(std pmap)`, `(std pvec)` | Immutable audit trail |\n| Stream live updates | `(std net fiber-ws)` | WebSocket on same server |\n| Sandbox user filters | `(std security sandbox)` | Landlock/seccomp per eval |\n| Manage lifecycle | `(std component)` | Clean start/stop ordering |\n| Deploy anywhere | `make static` | One musl binary, scp it |\n\n---\n\n## Architecture\n\n```\n                    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                    \u2502      fiber-httpd           \u2502\n                    \u2502   (one fiber per conn)     \u2502\n                    \u2502   epoll-backed accept      \u2502\n                    \u2514\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                          \u2502            \u2502\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u25bc                                    \u25bc\n     POST /hooks/:type                    GET /dashboard\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n     \u2502 1. Parse JSON    \u2502                 \u2502 WebSocket       \u2502\n     \u2502 2. Validate sig  \u2502                 \u2502 upgrade         \u2502\n     \u2502 3. Respond 202   \u2502                 \u2502 (fiber-ws)      \u2502\n     \u2502 4. Enqueue       \u2502                 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                         \u2502\n              \u2502                                   \u2502\n              \u25bc                                   \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u2502\n     \u2502 Ingest Channel           \u2502                 \u2502\n     \u2502 (std csp clj)            \u2502                 \u2502\n     \u2502                          \u2502                 \u2502\n     \u2502 Transducer pipeline:     \u2502                 \u2502\n     \u2502  validate \u2192 normalize    \u2502                 \u2502\n     \u2502  \u2192 deduplicate \u2192 route   \u2502                 \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                 \u2502\n              \u2502                                   \u2502\n              \u25bc                                   \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u2502\n     \u2502 Worker Pool              \u2502                 \u2502\n     \u2502 (actor supervisor)       \u2502                 \u2502\n     \u2502                          \u2502                 \u2502\n     \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510   \u2502                 \u2502\n     \u2502 \u2502worker-1\u2502 \u2502worker-2\u2502..N\u2502                 \u2502\n     \u2502 \u2514\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518   \u2502                 \u2502\n     \u2502     \u2502          \u2502        \u2502                 \u2502\n     \u2502  process    process     \u2502                 \u2502\n     \u2502  webhook    webhook     \u2502                 \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                 \u2502\n              \u2502                                  \u2502\n              \u25bc                                  \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                \u2502\n     \u2502 State Store              \u2502                \u2502\n     \u2502 (STM refs + pmap)        \u2502     updates    \u2502\n     \u2502                          \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n     \u2502 Immutable snapshots      \u2502\n     \u2502 of all webhook events    \u2502\n     \u2502 + processing results     \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n### Data Flow\n\n1. **Ingest:** fiber-httpd receives POST, one fiber handles the connection.\n   Handler parses JSON, validates HMAC signature, writes a `202 Accepted`\n   response immediately, then enqueues the event onto the ingest channel.  The\n   fiber is freed in microseconds \u2014 no blocking on processing.\n\n2. **Pipeline:** The ingest channel has a transducer stack attached:\n   - `(filtering valid-signature?)` \u2014 drop replayed/forged events\n   - `(mapping normalize-event)` \u2014 canonicalize field names, timestamps\n   - `(deduplicate)` \u2014 idempotency via event ID\n   - `(mapping route-event)` \u2014 tag with destination handler\n\n3. **Processing:** A supervisor manages N worker actors.  Each worker pulls from\n   the channel, executes the handler, writes results to the state store.  If a\n   worker crashes (bad payload, handler bug), the supervisor restarts it.  The\n   other workers continue unaffected.\n\n4. **State:** All webhook events and results live in STM-managed persistent maps.\n   `(dosync (alter events conj new-event))`.  No locks, no race conditions.\n   Every state transition is an immutable snapshot \u2014 you get a full audit trail\n   for free.\n\n5. **Dashboard:** A WebSocket endpoint streams state changes to connected\n   browsers.  Each WS connection is a fiber.  When the STM refs change, watchers\n   push updates to all connected dashboard fibers.\n\n6. **Sandboxing (optional):** Users can define filter expressions that run in a\n   Landlock sandbox.  `(run-safe-eval user-filter-code config)` executes in a\n   forked child with syscall restrictions.  The parent process is never at risk.\n\n---\n\n## API Design\n\n### Starting the Service\n\n```scheme\n#!/usr/bin/env scheme --libdirs lib --script\n(import (jerboa prelude)\n        (std clojure)\n        (std component)\n        (std net fiber-httpd)\n        (std net fiber-ws)\n        (std net router)\n        (std csp clj)\n        (std actor)\n        (std stm)\n        (std transducer)\n        (std text json)\n        (std crypto hmac))\n\n(def (main)\n  (let ([sys (-> (system-map\n                   'config    (component 'config)\n                   'state     (component 'state)\n                   'ingest    (component 'ingest)\n                   'workers   (component 'workers)\n                   'http      (component 'http))\n                 (system-using\n                   '((state   . (config))\n                     (ingest  . (config state))\n                     (workers . (config state ingest))\n                     (http    . (config state ingest)))))])\n    (displayln \"[edge] starting on port 8080...\")\n    (start sys)))\n\n(main)\n```\n\n### Webhook Ingestion Endpoint\n\n```scheme\n(def (handle-webhook req)\n  ;; 1. Parse the JSON body\n  (let* ([body     (request-body req)]\n         [payload  (string->json-object body)]\n         [type     (route-param req \"type\")]\n         [sig      (request-header req \"x-signature\")])\n\n    ;; 2. Verify HMAC signature\n    (unless (verify-hmac-sha256 secret sig body)\n      (respond-json 401 \"{\\\"error\\\":\\\"invalid signature\\\"}\"))\n\n    ;; 3. Build internal event record\n    (let ([event (hash-map\n                   \"id\"         (hash-ref payload \"id\" (uuid))\n                   \"type\"       type\n                   \"payload\"    payload\n                   \"received\"   (datetime->iso8601 (datetime-now))\n                   \"status\"     \"queued\")])\n\n      ;; 4. Enqueue for async processing \u2014 this returns immediately\n      (>!! ingest-ch event)\n\n      ;; 5. Respond 202 before processing starts\n      (respond-json 202\n        (json-object->string\n          (hash-map \"status\" \"queued\"\n                    \"id\"     (hash-ref event \"id\")))))))\n```\n\n### Transducer Pipeline\n\n```scheme\n(def ingest-xf\n  (compose-transducers\n    ;; Drop events with duplicate IDs (idempotency)\n    (deduplicate)\n\n    ;; Normalize: ensure all events have a timestamp\n    (mapping\n      (lambda (evt)\n        (if (hash-key? evt \"timestamp\")\n          evt\n          (let ([ht (make-hash-table)])\n            (hash-for-each (lambda (k v) (hash-put! ht k v)) evt)\n            (hash-put! ht \"timestamp\" (hash-ref evt \"received\"))\n            ht))))\n\n    ;; Drop obviously malformed events\n    (filtering\n      (lambda (evt)\n        (and (hash-key? evt \"type\")\n             (hash-key? evt \"payload\"))))))\n\n;; Create the ingest channel with the transducer attached\n(def ingest-ch (chan 4096 ingest-xf))\n```\n\n### Supervised Worker Pool\n\n```scheme\n(def (make-worker-behavior state-ref ingest-ch handlers)\n  ;; Each worker is an actor that loops reading from the channel\n  (lambda (msg)\n    (match msg\n      ['start\n       ;; Enter processing loop\n       (let loop ()\n         (let ([event (<!! ingest-ch)])\n           (when event\n             ;; Look up the handler for this event type\n             (let* ([type    (hash-ref event \"type\" \"unknown\")]\n                    [handler (hash-ref handlers type default-handler)]\n                    [result  (try\n                               (ok (handler event))\n                               (catch (e)\n                                 (err (error-message e))))])\n\n               ;; Record result in STM state\n               (dosync\n                 (alter state-ref\n                   (lambda (st)\n                     (let ([id (hash-ref event \"id\")])\n                       (hash-put! st id\n                         (hash-map\n                           \"event\"     event\n                           \"result\"    result\n                           \"processed\" (datetime->iso8601\n                                         (datetime-now))))\n                       st))))\n\n               ;; Notify dashboard watchers\n               (notify-watchers! event result))\n\n             (loop))))]\n\n      ;; Graceful shutdown\n      ['stop (void)])))\n\n(def (start-worker-pool n state-ref ingest-ch handlers)\n  (let ([children\n         (map (lambda (i)\n                (make-child-spec\n                  (string->symbol (format \"worker-~a\" i))\n                  (lambda ()\n                    (let ([ref (spawn-actor\n                                 (make-worker-behavior\n                                   state-ref ingest-ch handlers)\n                                 (format \"worker-~a\" i))])\n                      (send ref 'start)\n                      ref))\n                  'permanent     ;; always restart on crash\n                  5              ;; 5s shutdown grace\n                  'worker))\n              (iota n))])\n    (start-supervisor 'one-for-one children 10 60)))\n```\n\n### STM State Store\n\n```scheme\n;; The entire application state is one STM ref holding a persistent map.\n;; Every mutation is atomic, consistent, and produces an immutable snapshot.\n\n(def state-ref (make-ref (make-hash-table)))\n\n;; Read current state (outside transaction \u2014 snapshot)\n(def (get-event id)\n  (hash-get (ref-deref state-ref) id))\n\n;; Read all events for a type\n(def (get-events-by-type type)\n  (let ([st (ref-deref state-ref)])\n    (filter\n      (lambda (entry)\n        (let ([evt (hash-ref (cdr entry) \"event\")])\n          (equal? (hash-ref evt \"type\") type)))\n      (hash->list st))))\n\n;; Stats: count by status\n(def (event-stats)\n  (let ([st (ref-deref state-ref)])\n    (let ([total   0]\n          [ok-n    0]\n          [err-n   0])\n      (hash-for-each\n        (lambda (id record)\n          (set! total (+ total 1))\n          (let ([r (hash-ref record \"result\")])\n            (if (ok? r)\n              (set! ok-n (+ ok-n 1))\n              (set! err-n (+ err-n 1)))))\n        st)\n      (hash-map \"total\" total\n                \"ok\"    ok-n\n                \"error\" err-n))))\n```\n\n### WebSocket Dashboard\n\n```scheme\n;; Connected dashboard clients \u2014 a simple list protected by STM\n(def watchers-ref (make-ref '()))\n\n(def (handle-dashboard req fd poller)\n  ;; Upgrade HTTP \u2192 WebSocket\n  (let ([ws (fiber-ws-upgrade (request-headers req) fd poller)])\n    (when ws\n      ;; Register this connection\n      (dosync (alter watchers-ref (lambda (ws-list) (cons ws ws-list))))\n\n      ;; Send current state snapshot\n      (fiber-ws-send ws\n        (json-object->string (event-stats)))\n\n      ;; Keep alive \u2014 recv loop handles pings and detects disconnect\n      (let loop ()\n        (let ([msg (fiber-ws-recv ws)])\n          (cond\n            [(not msg)\n             ;; Client disconnected \u2014 unregister\n             (dosync\n               (alter watchers-ref\n                 (lambda (ws-list)\n                   (filter (lambda (w) (not (eq? w ws))) ws-list))))]\n            [else (loop)]))))))\n\n;; Called by workers after processing an event\n(def (notify-watchers! event result)\n  (let ([update (json-object->string\n                  (hash-map\n                    \"type\"   \"event-processed\"\n                    \"id\"     (hash-ref event \"id\")\n                    \"status\" (if (ok? result) \"ok\" \"error\")\n                    \"time\"   (datetime->iso8601 (datetime-now))))])\n    (for-each\n      (lambda (ws)\n        (guard (_ [#t (void)])  ;; ignore send errors on dead connections\n          (fiber-ws-send ws update)))\n      (ref-deref watchers-ref))))\n```\n\n### Sandboxed User Filters\n\n```scheme\n;; Users can submit filter expressions that run in a Landlock sandbox.\n;; The filter receives the event as a hash table binding `evt` and must\n;; return #t (accept) or #f (drop).\n\n(def (eval-user-filter filter-code event)\n  (let ([code (format\n                \"(let ([evt '~s]) ~a)\"\n                (hash->list event)\n                filter-code)]\n        [config (make-sandbox-config\n                  'timeout 2           ;; 2 second hard limit\n                  'seccomp 'pure-computation  ;; no I/O, no network\n                  'max-output-size 1024)])\n    (run-safe-eval code config)))\n\n;; Example usage:\n;; User submits: (equal? (cdr (assoc \"type\" evt)) \"payment.completed\")\n;; This runs in a forked child with:\n;;   - No filesystem access (Landlock)\n;;   - No network syscalls (seccomp)\n;;   - 2 second timeout\n;;   - Parent process is never at risk\n```\n\n### HTTP Router Assembly\n\n```scheme\n(def (make-edge-router state-ref ingest-ch)\n  (let ([r (make-router)])\n\n    ;; Webhook ingestion\n    (router-post! r \"/hooks/:type\"\n      (lambda (req) (handle-webhook req)))\n\n    ;; Query API\n    (router-get! r \"/api/events/:id\"\n      (lambda (req)\n        (let ([id (route-param req \"id\")])\n          (aif (get-event id)\n            (respond-json 200 (json-object->string it))\n            (respond-json 404 \"{\\\"error\\\":\\\"not found\\\"}\")))))\n\n    (router-get! r \"/api/stats\"\n      (lambda (req)\n        (respond-json 200\n          (json-object->string (event-stats)))))\n\n    ;; Dashboard WebSocket\n    (router-get! r \"/dashboard\"\n      (lambda (req)\n        (make-websocket-response\n          (lambda (fd poller req)\n            (handle-dashboard req fd poller)))))\n\n    ;; Health check\n    (router-get! r \"/health\"\n      (lambda (req)\n        (respond-json 200 \"{\\\"status\\\":\\\"ok\\\"}\")))\n\n    r))\n```\n\n### Full Startup\n\n```scheme\n(def (run-edge port num-workers)\n  (let* ([state-ref   (make-ref (make-hash-table))]\n         [ingest-ch   (chan 4096 ingest-xf)]\n         [handlers    (make-hash-table)]\n         [router      (make-edge-router state-ref ingest-ch)])\n\n    ;; Register webhook handlers by type\n    (hash-put! handlers \"payment.completed\"\n      (lambda (evt)\n        (displayln \"[payment] processing \" (hash-ref evt \"id\"))))\n\n    (hash-put! handlers \"user.created\"\n      (lambda (evt)\n        (displayln \"[user] processing \" (hash-ref evt \"id\"))))\n\n    ;; Start supervised worker pool\n    (let ([supervisor\n           (start-worker-pool num-workers state-ref ingest-ch handlers)])\n\n      ;; Start HTTP server on fibers\n      (displayln \"[edge] listening on :\" port)\n      (displayln \"[edge] \" num-workers \" workers supervised\")\n      (displayln \"[edge] dashboard at ws://localhost:\" port \"/dashboard\")\n\n      (fiber-httpd-start port\n        (lambda (req)\n          (let ([match (router-match router\n                         (request-method req)\n                         (request-path-only req))])\n            (if match\n              ((route-match-handler match) req)\n              (respond-json 404 \"{\\\"error\\\":\\\"not found\\\"}\"))))))))\n\n;; Entry point\n(run-edge 8080 4)\n```\n\n---\n\n## The Comparison That Sells It\n\n### Clojure Equivalent\n\nTo build the same webhook processor in Clojure, you need:\n\n```\nproject.clj (or deps.edn)          \u2014 dependency management\n\u251c\u2500\u2500 JVM 17+                        \u2014 200MB runtime\n\u251c\u2500\u2500 ring/ring-core                 \u2014 HTTP abstraction\n\u251c\u2500\u2500 http-kit or aleph              \u2014 async HTTP server\n\u251c\u2500\u2500 compojure or reitit            \u2014 routing\n\u251c\u2500\u2500 cheshire                       \u2014 JSON parsing\n\u251c\u2500\u2500 core.async                     \u2014 channels\n\u251c\u2500\u2500 mount or component             \u2014 lifecycle\n\u251c\u2500\u2500 redis (Carmine)                \u2014 job queue\n\u251c\u2500\u2500 sidekiq or machinery           \u2014 background workers\n\u251c\u2500\u2500 timbre                         \u2014 logging\n\u2514\u2500\u2500 Docker + docker-compose.yml    \u2014 deployment\n```\n\n**Result:**\n- 12+ dependencies (each with transitive deps \u2192 100+ JARs)\n- 3 processes minimum (JVM app + Redis + worker process)\n- 30-second cold start (JVM + dependency loading)\n- 512MB-2GB RAM (JVM heap + Redis)\n- Docker image: 400MB-1GB\n- `docker-compose up` to run locally\n- 5-10 files of boilerplate before writing business logic\n\n### Jerboa Equivalent\n\n```\nwebhook-service.ss                  \u2014 the entire service\n```\n\n**Result:**\n- 0 external dependencies (everything is stdlib)\n- 1 process (one binary does HTTP + workers + state + WebSocket)\n- Instant startup (Chez native code, no interpreter warmup)\n- 20-40MB RAM (no GC pressure from boxed JVM objects)\n- Static binary: 15MB\n- `scp edge user@server: && ssh server ./edge` to deploy\n- ~380 lines, one file, zero config files\n\n### Side-by-Side: Processing a Webhook\n\n**Clojure:**\n```clojure\n;; deps: ring, compojure, cheshire, core.async, carmine (redis)\n\n(ns webhook.handler\n  (:require [compojure.core :refer [defroutes POST]]\n            [ring.middleware.json :refer [wrap-json-body]]\n            [cheshire.core :as json]\n            [clojure.core.async :as async]\n            [taoensso.carmine :as redis]))\n\n(defroutes app-routes\n  (POST \"/hooks/:type\" [type :as req]\n    (let [payload (:body req)]\n      ;; Push to Redis queue (external process!)\n      (redis/wcar {} (redis/lpush \"webhooks\" (json/encode payload)))\n      {:status 202\n       :body (json/encode {:status \"queued\"})})))\n\n;; Separate worker process reads from Redis:\n(defn worker []\n  (loop []\n    (let [raw (redis/wcar {} (redis/brpop \"webhooks\" 0))\n          event (json/decode (second raw) true)]\n      (process-event! event)\n      (recur))))\n```\n\n**Jerboa:**\n```scheme\n;; deps: none (everything is stdlib)\n\n(import (jerboa prelude)\n        (std clojure)\n        (std net fiber-httpd)\n        (std csp clj)\n        (std text json))\n\n(def work-ch (chan 4096))\n\n(def (handle-webhook req)\n  (let ([payload (string->json-object (request-body req))])\n    (>!! work-ch payload)\n    (respond-json 202 \"{\\\"status\\\":\\\"queued\\\"}\")))\n\n;; Workers are fibers on the same process \u2014 no Redis, no IPC\n(dotimes (i 4)\n  (go (let loop ()\n        (let ([event (<!! work-ch)])\n          (when event\n            (process-event! event)\n            (loop))))))\n\n(fiber-httpd-start 8080 handle-webhook)\n```\n\nThe Jerboa version is **12 lines** vs Clojure's **20+ lines + Redis + a separate\nworker process**.  And the Jerboa version handles more concurrent connections\n(fibers vs threads), has built-in fault tolerance (supervisor), and deploys as\none file.\n\n---\n\n## Implementation Plan\n\n### Phase 1: Core Demo (the ~380-line file) \u2014 COMPLETE\n\n**Deliverable:** [`jerboa-edge/edge.ss`](https://git.sr.ht/~lisp/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa-edge\ncd jerboa-edge\nmake run\n\n# In another terminal:\ncurl -X POST localhost:8080/hooks/payment.completed \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"evt_123\",\"amount\":4999}'\n\ncurl localhost:8080/api/stats\n# => {\"total\":1,\"ok\":1,\"error\":0}\n```\n\n**Scope:**\n\n- [x] 1.1 **HTTP server with routing** \u2014 fiber-httpd with `:param` pattern routing.\n    Five endpoints: `POST /hooks/:type`, `GET /api/events/:id`, `GET /api/stats`,\n    `GET /health`, `GET /dashboard`.\n\n- [x] 1.2 **Channel-based ingestion** \u2014 CSP channel with transducer pipeline\n    (validate + dedup-by-ID + normalize).  Decouples accept from process.\n\n- [x] 1.3 **Actor-supervised workers** \u2014 N worker actors under a one-for-one\n    supervisor.  Each pulls from the channel, processes, writes to state.\n    `POST /hooks/test.crash` deliberately crashes a worker to show supervisor restart.\n\n- [x] 1.4 **STM state store** \u2014 All state in `(make-ref (make-persistent-map))`.\n    Atomic updates via `dosync`/`alter` with `(std pmap)` persistent maps.\n    Immutable snapshots \u2014 safe for concurrent reads during writes.\n\n- [x] 1.5 **WebSocket dashboard** \u2014 Single WS endpoint that streams\n    event-processed notifications to connected clients.  Sends initial stats\n    on connect.\n\n- [x] 1.6 **HMAC signature verification** \u2014 Validate `X-Signature` header using\n    `(std crypto native)` HMAC-SHA256.  Constant-time comparison.\n    Configurable via `EDGE_SECRET` env var.\n\n**Non-goals for Phase 1:**\n- Persistence across restarts (in-memory only)\n- TLS termination (use a reverse proxy, or wait for Phase 3)\n- Static binary packaging (Phase 3)\n\n**Success criteria:**\n- [x] Runs with `make run` (382 lines of Scheme)\n- [x] Handles 10K concurrent webhook POSTs without dropped events (verified: 9,993/10,000 accepted at 200 concurrent connections, ~454 req/sec)\n- [x] Worker crash + supervisor restart visible in logs\n- [x] WebSocket dashboard receives live event stream (verified with websocket-client)\n- [x] `curl` examples work out of the box (17/17 smoke tests pass)\n- [x] HMAC: no-sig and wrong-sig return 401, valid-sig returns 202\n\n### Phase 2: Programmability\n\n**Deliverable:** Sandboxed user filter support + handler registry\n\n- [x] 2.1 **Sandboxed eval** \u2014 Users submit filter expressions via\n    `POST /api/filters`.  Filters evaluated per-event via\n    `restricted-eval-string` (allowlist-only: no I/O, no FFI) with a\n    2-second engine-based timeout (no fork \u2014 fiber-safe).  Events that\n    fail any filter are dropped silently before processing.\n\n    ```\n    POST /api/filters  {\"name\":\"payments-only\",\"code\":\"(equal? (cdr (assoc \\\"type\\\" evt)) \\\"payment.completed\\\")\"}\n    GET  /api/filters\n    DELETE /api/filters/:name\n    ```\n\n    Key implementation detail: event data is bound as `evt` (a deep\n    alist), so filter code uses `assoc` with string keys (not `assq`).\n\n- [x] 2.2 **Hot handler registration** \u2014 New webhook types registered at\n    runtime via `POST /api/handlers`.  No restart required.  Handler\n    code runs in a custom restricted environment (`safe-bindings` +\n    `displayln`) with a 5-second engine timeout.  Exceptions propagate\n    to the worker's retry machinery.\n\n    ```\n    POST /api/handlers  {\"type\":\"order.shipped\",\"code\":\"(displayln \\\"shipped: \\\" (cdr (assoc \\\"id\\\" evt)))\"}\n    GET  /api/handlers\n    ```\n\n- [x] 2.3 **Retry with exponential backoff** \u2014 Failed events re-enter the\n    channel with an incremented retry count.  Workers delay via\n    `fork-thread` + `sleep` (non-blocking, OS-thread safe).  Dedup\n    transducer passes retried events (`retry-count > 0`).  After 3\n    retries, events go to a dead-letter STM store.\n\n    ```\n    GET  /api/dead-letter\n    POST /api/dead-letter/:id/replay\n    ```\n\n    Replay sets `retry-count: 1` to bypass the dedup filter.\n\n**Success criteria:**\n- [x] Filter code cannot call `open-file`, `socket`, or any I/O (allowlist env)\n- [x] Filter with 2s timeout verified: `with-timeout` uses Chez engines, no fork\n- [x] Hot-registered handlers process events without service restart (verified: `order.shipped`, `inventory.updated`)\n- [x] Failed events retry 3\u00d7 with 1s/2s/4s backoff then dead-letter (verified in log)\n- [x] Dead-letter replay re-processes event and clears queue (verified: status \u2192 \"ok\")\n- [x] Runs with `make run` (690 lines of Scheme)\n\n### Phase 3: Production Hardening \u2713\n\n**Deliverable:** Static binary, TLS, persistence, metrics (~1048 lines)\n\n- [x] 3.1 **Static musl binary** \u2014 `make edge-static` produces a fully static\n    ELF via `docker run jerboa21/jerboa` (musl Chez + Rust toolchain + WPO).\n    **Verified: 12MB, zero runtime dependencies.**  All native FFI symbols\n    registered via `Sforeign_symbol`; `JERBOA_STATIC=1` auto-set at startup\n    to bypass `load-shared-object` calls.  Deploy with `scp edge host:/usr/local/bin/`.\n\n- [x] 3.2 **TLS termination** \u2014 Optional `EDGE_TLS_CERT` / `EDGE_TLS_KEY`\n    env vars for direct HTTPS.  Uses `(std net tls-rustls)` \u2014 the Rust\n    rustls backend.  `make run-tls` target.  One OS thread per TLS connection;\n    same router handler for both plain and TLS paths.\n\n- [x] 3.3 **SQLite persistence** \u2014 `EDGE_DB_PATH` env var enables persistence\n    via `(std db sqlite-native)` (bundled rusqlite, no external libsqlite3).\n    WAL mode for concurrent reads.  Events and dead-letters loaded into STM\n    on startup.  All writes are fire-and-forget (guarded; SQLite error never\n    crashes a worker).  `make run-db` target.\n\n- [x] 3.4 **Prometheus metrics** \u2014 `/metrics` endpoint in Prometheus text\n    format via `(std metrics)`.  Exposed counters and histogram:\n    - `edge_events_received_total`\n    - `edge_events_ok_total`\n    - `edge_events_error_total`\n    - `edge_events_dead_letter_total`\n    - `edge_worker_restarts_total`\n    - `edge_processing_duration_seconds` (histogram, default buckets)\n    - `edge_active_connections` (gauge)\n\n- [x] 3.5 **Structured JSON logging** \u2014 All event-path log lines are JSON\n    (`jlog` helper, `json-object->string`).  Fields: `ts`, `msg`, plus\n    context fields (`worker`, `id`, `type`, `status`, `ms`, `err`).\n    Startup banner stays human-readable.\n\n- [x] 3.6 **Graceful shutdown** \u2014 `SIGTERM` triggers via `add-signal-handler!`\n    from `(std os signal)`.  Sequence: `fiber-httpd-stop!` \u2192 `chan-close!`\n    (workers see EOF) \u2192 3s drain window \u2192 `PRAGMA wal_checkpoint(FULL)` \u2192\n    `(exit 0)`.  Verified: completes in ~3s.\n\n**Success criteria:**\n- [x] `make edge-static` builds and runs \u2014 12MB, fully static, verified functional\n- [x] Graceful shutdown completes in ~3 seconds (verified)\n- [x] Prometheus scrape at `/metrics` returns well-formed text format (verified)\n- [x] SQLite events survive restart: `total:2 ok:2` after reload (verified)\n- [x] JSON log lines include worker, event id, type, status, duration (verified)\n- [x] Runs with `make run` (~1048 lines of Scheme)\n\n### Phase 4: Distribution \u2713\n\n**Deliverable:** Multi-node webhook processing via authenticated TCP transport and in-process Raft (~1244 lines)\n\n- [x] 4.1 **Clustered actors via `(std actor transport)`** \u2014 When\n    `EDGE_CLUSTER_COOKIE` is set, each node registers itself with\n    `start-node!` and starts a TCP server with `start-node-server!`.\n    Transport uses HMAC-SHA256 4-step challenge-response authentication\n    with per-message integrity (derived session key).  Each node spawns\n    a named `'edge-remote-ingest` actor that receives `(work event)`\n    messages from peers and enqueues them into the local ingest channel.\n    After HTTP starts, `connect-to-peers!` calls `GET /api/cluster/info`\n    on each peer to discover actor IDs, then builds remote refs via\n    `make-remote-actor-ref`.  Incoming webhooks are round-robined across\n    self + connected peers; failed peer sends fall back to local.\n    Wire format: `[4-byte BE length][fasl body][32-byte HMAC-SHA256]`.\n\n    ```bash\n    # Terminal 1 \u2014 node A (HTTP :8080, transport :9000)\n    make run-cluster-a\n\n    # Terminal 2 \u2014 node B (HTTP :8081, transport :9001)\n    make run-cluster-b\n\n    # POST to either node; events processed on either worker pool\n    curl -X POST localhost:8080/hooks/payment.completed -d '{\"id\":\"e1\",...}'\n    curl localhost:8080/api/cluster/info   # shows node_id, actor_id, peers\n    ```\n\n- [x] 4.2 **Replicated event ordering via `(std raft)`** \u2014 When\n    `EDGE_RAFT_SIZE > 0`, an N-node in-process Raft cluster runs alongside\n    the service.  Every accepted webhook is proposed to the Raft commit\n    log by the current leader (`raft-propose!`), providing a consensus-\n    ordered event sequence.  The commit log accumulates in-memory and is\n    available via `raft-log`/`raft-commit-index` on the leader node.\n\n    Note: `(std raft)` uses in-memory channels (no TCP) \u2014 it is an\n    in-process simulation designed to be bridged over a transport layer\n    for true cross-process Raft.  `(std crdt)` does not exist in the\n    stdlib; per-node STM with coordinator routing is used instead.\n\n    Note: `(std raft)` uses in-memory channels \u2014 cross-process Raft\n    would require bridging node inboxes over `(std actor transport)`.\n    `(std crdt)` does not exist in the stdlib.\n\n    ```bash\n    make run-raft RAFT_SIZE=3\n    curl localhost:8080/api/cluster/info\n    # => {\"raft_nodes\":3,\"raft_leader\":\"yes\",\"raft_term\":3,\"raft_commit\":5,...}\n    ```\n\n- [x] 4.3 **`GET /api/cluster/info`** \u2014 New endpoint returns full cluster\n    state: `node_id`, `ingest_actor_id`, `cluster_enabled`, `peers_connected`,\n    `raft_nodes`, `raft_leader`, `raft_term`, `raft_commit`.  Used by\n    peers at startup to discover actor IDs for remote refs.\n\n- [x] 4.4 **Transport shutdown** \u2014 `graceful-shutdown!` calls\n    `transport-shutdown!` (closes all authenticated TCP connections) and\n    `raft-stop!` on each Raft node before exit.\n\n**Success criteria:**\n- [x] `EDGE_CLUSTER_COOKIE=x make run` starts transport server on port 9000\n- [x] `make run-raft RAFT_SIZE=3` shows \"leader: elected, term: 3\" in logs\n- [x] `/api/cluster/info` returns raft_leader, raft_term, raft_commit (verified)\n- [x] Webhook processed correctly with Raft enabled (verified: status \"ok\")\n- [x] Graceful shutdown closes transport + stops Raft nodes\n- [x] Runs with `make run` (~1244 lines of Scheme)\n\n---\n\n## Module Dependency Map\n\nOnly stdlib modules \u2014 zero external dependencies:\n\n```\nexamples/webhook-service.ss\n\u251c\u2500\u2500 (jerboa prelude)              ;; base language\n\u251c\u2500\u2500 (std clojure)                 ;; polymorphic ops, inc/dec, etc.\n\u251c\u2500\u2500 (std component)               ;; lifecycle management\n\u251c\u2500\u2500 (std net fiber-httpd)         ;; HTTP server (fiber-native)\n\u2502   \u2514\u2500\u2500 (std net io)              ;; epoll-backed fiber I/O\n\u251c\u2500\u2500 (std net fiber-ws)            ;; WebSocket (fiber-native)\n\u251c\u2500\u2500 (std net router)              ;; URL routing with params\n\u251c\u2500\u2500 (std csp clj)                 ;; Clojure-style channels\n\u2502   \u251c\u2500\u2500 (std csp)                 ;; base CSP\n\u2502   \u251c\u2500\u2500 (std csp select)          ;; alts!/timeout\n\u2502   \u251c\u2500\u2500 (std csp fiber-chan)      ;; fiber-aware channels\n\u2502   \u2514\u2500\u2500 (std fiber)               ;; M:N fiber runtime\n\u251c\u2500\u2500 (std actor)                   ;; actors + supervision\n\u251c\u2500\u2500 (std stm)                     ;; transactional memory\n\u251c\u2500\u2500 (std transducer)              ;; composable transforms\n\u251c\u2500\u2500 (std text json)               ;; JSON parser/writer\n\u251c\u2500\u2500 (std crypto hmac)             ;; HMAC-SHA256 verification\n\u251c\u2500\u2500 (std security sandbox)        ;; Landlock/seccomp eval\n\u2514\u2500\u2500 (std pmap)                    ;; persistent hash maps\n```\n\nAll of these ship with Jerboa.  `(std crypto hmac)` and\n`(std security sandbox)` use the Rust native backend; everything else is\npure Scheme.\n\n---\n\n## Benchmarking Targets\n\nThese numbers establish what \"production-grade\" means for the demo:\n\n| Metric | Target | How to measure |\n|---|---|---|\n| Webhook throughput | 50K events/sec sustained | `wrk -c 1000 -t 8 -d 30s` |\n| p99 accept latency | < 5ms | Measure time from connect to 202 |\n| Memory (100K queued) | < 100MB | `/proc/self/status` VmRSS |\n| Worker restart time | < 50ms | Log timestamp delta after kill |\n| WebSocket fan-out | 1000 clients, < 10ms broadcast | Measure from STM commit to WS send |\n| Cold start | < 100ms | Time from exec to first request served |\n| Binary size | < 25MB | `ls -la edge` |\n| Concurrent connections | 100K+ | `ulimit -n` + `wrk` |\n\n### Benchmark Script\n\n```bash\n#!/bin/bash\n# bench-edge.sh \u2014 run against a live edge instance\n\necho \"=== Throughput ===\"\nwrk -c 500 -t 4 -d 10s -s post.lua http://localhost:8080/hooks/test\n\necho \"=== Latency ===\"\nwrk -c 1 -t 1 -d 5s -s post.lua --latency http://localhost:8080/hooks/test\n\necho \"=== Memory ===\"\nps -o rss= -p $(pgrep -f webhook-service) | awk '{print $1/1024 \"MB\"}'\n\necho \"=== Stats ===\"\ncurl -s localhost:8080/api/stats | python3 -m json.tool\n```\n\n---\n\n## What This Proves\n\nWhen someone sees this demo and asks \"why not just use Clojure?\", the answer\nis concrete:\n\n1. **Deploy in 5 seconds, not 5 minutes.** One binary, scp, run.  No JVM, no\n   Docker, no Redis, no config files.\n\n2. **20MB of RAM, not 2GB.**  Fibers are 4KB each.  Persistent maps share\n   structure.  No JVM heap overhead, no Redis process.\n\n3. **100ms cold start, not 30 seconds.**  Chez compiles to native code.  No\n   classloading, no dependency injection framework initialization.\n\n4. **Fault tolerance is built in, not bolted on.**  Actor supervision trees\n   restart crashed workers.  STM prevents data races.  You don't need to\n   add \"resilience4j\" as a dependency.\n\n5. **Security is built in, not bolted on.**  Sandboxed eval uses kernel-level\n   isolation (Landlock/seccomp).  No \"eval is dangerous\" disclaimer \u2014 eval\n   is sandboxed by default.\n\n6. **One file, ~1244 lines.**  Phase 1 was ~380 lines.  Phase 4 adds TLS,\n   Prometheus metrics, SQLite persistence, graceful shutdown, distributed\n   actor transport with HMAC-SHA256 auth, and in-process Raft consensus \u2014\n   and the result is still one file a senior engineer can read in an\n   afternoon.  Try that with a Spring Boot webhook processor.\n\nThis isn't a toy.  It handles 50K events/sec, restarts crashed workers in\n50ms, streams live updates over WebSocket, scrapes cleanly into Prometheus,\nand the entire thing fits in a single file with zero external dependencies.\n\n---\n\n## Related Work\n\n- `docs/green-wins.md` \u2014 fiber-httpd production hardening roadmap\n- `docs/clojure-left.md` \u2014 Clojure gap analysis and priority roadmap\n- `docs/clojure-vs-jerboa.md` \u2014 feature parity comparison\n- `examples/hello-api.ss` \u2014 simple HTTP API example (starting point)\n- `examples/chat-server.ss` \u2014 WebSocket + concurrency example\n- `lib/std/component.sls` \u2014 Stuart Sierra lifecycle (used in Phase 1)\n"}
 {"text": "<!-- FILE: docs/concurrency-extended.md -->\n# Concurrency & Control Flow Extensions\n\nAdvanced concurrency primitives, resource management, and control flow operators\nfor Jerboa's standard library.\n\n## Table of Contents\n\n- [Event System (`std misc event`)](#event-system)\n- [Custodians (`std misc custodian`)](#custodians)\n- [Resource Pool (`std misc pool`)](#resource-pool)\n- [Delimited Continuations (`std misc delimited`)](#delimited-continuations)\n- [Continuation Marks (`std misc cont-marks`)](#continuation-marks)\n- [Non-deterministic Backtracking (`std misc amb`)](#non-deterministic-backtracking)\n\n---\n\n## Event System\n\n**Module:** `(std misc event)`\n**File:** `lib/std/misc/event.sls`\n\n```scheme\n(import (std misc event))\n```\n\n### Overview\n\nEvents are lazy values that may or may not be ready. They can be composed with\n`choice`, transformed with `wrap`/`handle`, and synchronized with `sync` and\n`sync/timeout`. Channels provide synchronous rendezvous-style communication\nbetween threads, built on events.\n\nThe design follows the Concurrent ML (CML) model: events are first-class values\nthat represent potential communications. They are not \"fired\" until synchronized.\n\n### API Reference\n\n| Procedure | Signature | Description |\n|-----------|-----------|-------------|\n| `make-event` | `(make-event poll-thunk)` | Create an event from a poll thunk. The thunk must return `(values ready? value)`. |\n| `event-ready?` | `(event-ready? evt)` | Poll the event once; returns `#t` if it is currently ready. |\n| `event-value` | `(event-value evt)` | Block (spin-wait with backoff) until the event fires, then return its value. |\n| `sync` | `(sync evt ...)` | Wait for any one of the given events to fire. Returns the value of the first ready event. Blocks indefinitely. |\n| `sync/timeout` | `(sync/timeout timeout-ms evt ...)` | Like `sync` but returns `#f` if no event fires within `timeout-ms` milliseconds. |\n| `choice` | `(choice evt ...)` | Combine multiple events into one. When polled, tries each in order and returns the first ready value. |\n| `wrap` | `(wrap evt proc)` | Transform an event's value: when `evt` fires with value `v`, the wrapped event fires with `(proc v)`. |\n| `handle` | `(handle evt proc)` | Alias for `wrap`. |\n| `always-event` | `(always-event val)` | An event that is immediately ready with `val`. |\n| `never-event` | `never-event` | A value (not a procedure). An event that is never ready. |\n| `timer-event` | `(timer-event delay-ms)` | An event that fires with `#t` after `delay-ms` milliseconds. |\n| `make-channel` | `(make-channel)` | Create a synchronous rendezvous channel. |\n| `channel-send` | `(channel-send ch val)` | Blocking send: deposits `val` and waits until a receiver consumes it. |\n| `channel-recv` | `(channel-recv ch)` | Blocking receive: waits for a sender and returns the sent value. |\n| `channel-send-event` | `(channel-send-event ch val)` | An event for sending `val`. Fires (with `(void)`) when a receiver is waiting. |\n| `channel-recv-event` | `(channel-recv-event ch)` | An event that fires with the received value when one is available. |\n\n### Examples\n\n**Basic event polling:**\n\n```scheme\n(import (std misc event))\n\n;; An event that is always ready\n(let ([e (always-event 42)])\n  (event-ready? e))       ; => #t\n  (event-value e))        ; => 42\n\n;; never-event is never ready\n(event-ready? never-event) ; => #f\n```\n\n**Timer with sync/timeout:**\n\n```scheme\n;; Wait up to 500ms for a timer that fires at 100ms\n(sync/timeout 500 (timer-event 100))  ; => #t (the timer's value)\n\n;; Timeout expires before the timer fires\n(sync/timeout 10 (timer-event 5000))  ; => #f\n```\n\n**Choosing between events:**\n\n```scheme\n;; First-ready-wins: timer vs. channel receive\n(let ([ch (make-channel)])\n  ;; In another thread: (channel-send ch \"hello\")\n  (sync\n    (wrap (timer-event 1000) (lambda (v) 'timeout))\n    (wrap (channel-recv-event ch) (lambda (msg) (list 'got msg)))))\n;; => 'timeout if nothing sent within 1s, or '(got \"hello\") if sent\n```\n\n**Rendezvous channels between threads:**\n\n```scheme\n(import (std misc event) (chezscheme))\n\n(let ([ch (make-channel)])\n  ;; Producer thread\n  (fork-thread\n    (lambda ()\n      (channel-send ch \"hello\")\n      (channel-send ch \"world\")))\n  ;; Consumer\n  (let ([a (channel-recv ch)]\n        [b (channel-recv ch)])\n    (list a b)))\n;; => (\"hello\" \"world\")\n```\n\n**Custom event from a poll thunk:**\n\n```scheme\n;; An event that fires when a file exists\n(define (file-exists-event path)\n  (make-event\n    (lambda ()\n      (if (file-exists? path)\n          (values #t path)\n          (values #f #f)))))\n\n(sync/timeout 5000 (file-exists-event \"/tmp/ready.flag\"))\n```\n\n---\n\n## Custodians\n\n**Module:** `(std misc custodian)`\n**File:** `lib/std/misc/custodian.sls`\n\n```scheme\n(import (std misc custodian))\n```\n\n### Overview\n\nCustodians are hierarchical resource groups inspired by Racket's custodian\nmodel. Every managed resource (ports, handles, custom objects) belongs to a\ncustodian. Shutting down a custodian recursively shuts down all its children and\nreleases all their resources. The `with-custodian` form provides automatic\ncleanup on normal exit, exceptions, or continuation escapes.\n\n### API Reference\n\n| Procedure / Syntax | Signature | Description |\n|--------------------|-----------|-------------|\n| `make-custodian` | `(make-custodian)` or `(make-custodian parent)` | Create a child custodian. Defaults to `(current-custodian)` as parent. |\n| `custodian?` | `(custodian? x)` | Returns `#t` if `x` is a custodian. |\n| `current-custodian` | `(current-custodian)` | Parameter holding the current custodian. |\n| `custodian-register!` | `(custodian-register! resource shutdown-proc)` or `(custodian-register! custodian resource shutdown-proc)` | Register a resource with a shutdown thunk. Returns the resource. If custodian is omitted, uses `(current-custodian)`. |\n| `custodian-shutdown-all` | `(custodian-shutdown-all c)` | Recursively shut down custodian `c`: close all resources, shut down all children, remove from parent. Errors during individual resource cleanup are swallowed so one failure does not prevent others from cleaning up. |\n| `custodian-managed-list` | `(custodian-managed-list c)` | Return a list of all managed resources and child custodians for `c`. |\n| `custodian-open-input-file` | `(custodian-open-input-file path)` or `(custodian-open-input-file path custodian)` | Open an input port registered with the custodian for automatic cleanup. |\n| `custodian-open-output-file` | `(custodian-open-output-file path)` or `(custodian-open-output-file path custodian)` | Open an output port registered with the custodian for automatic cleanup. |\n| `with-custodian` | `(with-custodian body ...)` | Run `body ...` under a fresh custodian. The custodian is shut down when the body exits (normally, by exception, or by continuation escape). |\n\n### Examples\n\n**Automatic cleanup with `with-custodian`:**\n\n```scheme\n(import (std misc custodian))\n\n(with-custodian\n  (let ([p (custodian-open-input-file \"data.txt\")])\n    (read p)))\n;; Port is automatically closed when with-custodian exits\n```\n\n**Hierarchical resource management:**\n\n```scheme\n(let ([parent (make-custodian)])\n  (parameterize ([current-custodian parent])\n    (let ([child (make-custodian)])\n      (parameterize ([current-custodian child])\n        ;; Register resources under the child custodian\n        (let ([handle (list 'connection)])\n          (custodian-register! handle (lambda () (display \"closed\\n\")))))\n      ;; Inspect what the parent manages\n      (custodian-managed-list parent)))\n  ;; Shut down the parent \u2014 all children and their resources are cleaned up\n  (custodian-shutdown-all parent))\n```\n\n**Registering custom resources:**\n\n```scheme\n(with-custodian\n  ;; Register a custom handle with a cleanup procedure\n  (let ([sock (open-tcp-connection \"example.com\" 80)])\n    (custodian-register! sock (lambda () (close-port sock)))\n    (put-bytevector sock #vu8(71 69 84))\n    (get-bytevector-all sock)))\n;; sock is closed automatically on exit\n```\n\n---\n\n## Resource Pool\n\n**Module:** `(std misc pool)`\n**File:** `lib/std/misc/pool.sls`\n\n```scheme\n(import (std misc pool))\n```\n\n### Overview\n\nA thread-safe, generic resource pool. Resources are created on demand up to a\nconfigurable maximum, reused when idle, and optionally evicted after an idle\ntimeout. The pool uses a mutex and condition variable internally, so `pool-acquire`\ncan block without busy-waiting when the pool is full.\n\n### API Reference\n\n| Procedure / Syntax | Signature | Description |\n|--------------------|-----------|-------------|\n| `make-pool` | `(make-pool creator destroyer max-size)` or `(make-pool creator destroyer max-size idle-timeout)` | Create a pool. `creator` is a thunk returning a new resource. `destroyer` takes a resource and frees it. `max-size` is the maximum total resources (idle + in-use). `idle-timeout` is `#f` (no expiry) or a number of seconds after which idle resources are destroyed. |\n| `pool?` | `(pool? x)` | Returns `#t` if `x` is a pool. |\n| `pool-acquire` | `(pool-acquire p)` or `(pool-acquire p timeout)` | Get a resource from the pool. Reuses an idle resource if available, creates a new one if below max, or blocks. `timeout` is `#f` (block forever) or seconds. Returns the resource, or `#f` on timeout. |\n| `pool-release` | `(pool-release p resource)` | Return a resource to the pool, making it available for others. |\n| `with-resource` | `(with-resource pool (var) body ...)` | Acquire a resource, bind it to `var`, evaluate `body ...`, and release the resource on exit (even if an exception is raised). Uses `dynamic-wind`. |\n| `pool-drain` | `(pool-drain p)` | Destroy all idle resources. In-use resources are not affected. |\n| `pool-stats` | `(pool-stats p)` | Returns an alist: `((total . N) (idle . N) (in-use . N))`. Also evicts expired idle resources before counting. |\n\n### Examples\n\n**Basic connection pool:**\n\n```scheme\n(import (std misc pool))\n\n(define db-pool\n  (make-pool\n    (lambda () (open-db-connection \"localhost:5432\"))  ; creator\n    (lambda (c) (close-db-connection c))               ; destroyer\n    10))                                                ; max 10 connections\n\n;; Acquire, use, release\n(let ([conn (pool-acquire db-pool)])\n  (query conn \"SELECT 1\")\n  (pool-release db-pool conn))\n```\n\n**Using `with-resource` for automatic release:**\n\n```scheme\n(with-resource db-pool (conn)\n  (query conn \"SELECT * FROM users WHERE id = 1\"))\n;; conn is released back to the pool even on error\n```\n\n**Acquire with timeout:**\n\n```scheme\n;; Wait at most 5 seconds for a resource\n(let ([conn (pool-acquire db-pool 5)])\n  (if conn\n      (begin (query conn \"SELECT 1\")\n             (pool-release db-pool conn))\n      (display \"pool exhausted, try again later\\n\")))\n```\n\n**Idle timeout for resource eviction:**\n\n```scheme\n;; Resources idle for more than 60 seconds are destroyed\n(define pool\n  (make-pool\n    (lambda () (make-fresh-resource))\n    (lambda (r) (destroy-resource r))\n    20     ; max-size\n    60))   ; idle-timeout in seconds\n```\n\n**Pool statistics:**\n\n```scheme\n(pool-stats db-pool)\n;; => ((total . 3) (idle . 1) (in-use . 2))\n```\n\n**Drain idle resources:**\n\n```scheme\n(pool-drain db-pool)\n;; All idle resources are destroyed; in-use resources are unaffected\n(pool-stats db-pool)\n;; => ((total . 2) (idle . 0) (in-use . 2))\n```\n\n---\n\n## Delimited Continuations\n\n**Module:** `(std misc delimited)`\n**File:** `lib/std/misc/delimited.sls`\n\n```scheme\n(import (std misc delimited))\n```\n\n### Overview\n\nProvides delimited continuations via `reset`/`shift` (Danvy and Filinski style)\nand a prompt-based API (`call-with-prompt`/`abort-to-prompt`).\n\n`reset` establishes a delimiter (prompt) around an expression. `shift` captures\nthe continuation up to the nearest enclosing `reset` as a procedure `k`. The\ncaptured continuation can be called zero, one, or multiple times.\n\nThe implementation uses the Filinski encoding on top of `call/cc`.\n\n### API Reference\n\n| Procedure / Syntax | Signature | Description |\n|--------------------|-----------|-------------|\n| `reset` | `(reset body ...)` | Establish a continuation delimiter. Returns the value of `body ...`, or the value passed to `shift` if `shift` does not invoke `k`. |\n| `shift` | `(shift k body ...)` | Capture the continuation up to the nearest `reset` as `k`, then evaluate `body ...`. If `k` is never called, the `reset` returns the result of `body ...`. |\n| `make-prompt-tag` | `(make-prompt-tag)` or `(make-prompt-tag name)` | Create a prompt tag for use with `call-with-prompt`. |\n| `call-with-prompt` | `(call-with-prompt tag thunk handler)` | Run `thunk` under a prompt identified by `tag`. If `abort-to-prompt` is called with the same `tag`, control returns to the prompt and the result is the value(s) passed to `abort-to-prompt`. |\n| `abort-to-prompt` | `(abort-to-prompt tag val ...)` | Abort to the nearest prompt matching `tag`, returning `val ...`. Raises an error if no matching prompt is found. |\n\n### Examples\n\n**Basic reset/shift:**\n\n```scheme\n(import (std misc delimited))\n\n;; shift captures the continuation (+ 1 []) up to reset\n(reset (+ 1 (shift k (k 10))))    ; => 11\n\n;; If k is not called, the reset returns the shift body's value\n(reset (+ 1 (shift k 42)))        ; => 42\n\n;; k can be called multiple times\n(reset (+ 1 (shift k (+ (k 10) (k 20)))))  ; => 32\n;; (k 10) => 11, (k 20) => 21, 11 + 21 = 32\n```\n\n**Building a list with shift:**\n\n```scheme\n;; Collect elements via shift\n(reset\n  (let ([x (shift k (cons 'a (k 'ignored)))])\n    (let ([y (shift k (cons 'b (k 'ignored)))])\n      '())))\n;; => (a b)\n```\n\n**Prompt-based abort:**\n\n```scheme\n(let ([tag (make-prompt-tag 'my-prompt)])\n  (call-with-prompt tag\n    (lambda ()\n      (+ 1 (abort-to-prompt tag 99)))\n    (lambda (v) v)))\n;; => 99\n```\n\n**Simulating exceptions with shift:**\n\n```scheme\n(define (try thunk handler)\n  (reset\n    (handler (shift k (k (thunk))))))\n\n;; Not really needed with Chez's guard, but shows the pattern\n```\n\n---\n\n## Continuation Marks\n\n**Module:** `(std misc cont-marks)`\n**File:** `lib/std/misc/cont-marks.sls`\n\n```scheme\n(import (std misc cont-marks))\n```\n\n### Overview\n\nContinuation marks let you attach key-value metadata to continuation frames.\nThis module re-exports Chez Scheme's native continuation mark support with\nSRFI 157 / Racket-compatible names. Continuation marks are useful for\nimplementing dynamic parameters, stack traces, profiling, and other\ncontext-passing patterns without modifying function signatures.\n\n**Important:** Chez Scheme uses `eq?` for key comparison. Use symbols or fixnums\nas keys. String or pair keys only work if the exact same object is used for\nboth setting and lookup.\n\n### API Reference\n\n| Procedure / Syntax | Signature | Description |\n|--------------------|-----------|-------------|\n| `with-continuation-mark` | `(with-continuation-mark key val body)` | Evaluate `body` in a context where the current continuation frame has `key` mapped to `val`. If a mark for `key` already exists on this frame, it is replaced. |\n| `current-continuation-marks` | `(current-continuation-marks)` | Capture the full set of continuation marks from the current continuation. |\n| `continuation-mark-set->list` | `(continuation-mark-set->list mark-set key)` | Extract all values for `key` from the mark set as a list, innermost first. |\n| `continuation-mark-set-first` | `(continuation-mark-set-first mark-set key)` | Return the first (innermost) value for `key` from the mark set, or `#f` if not found. `mark-set` can be `#f` to use the current continuation marks. |\n| `continuation-marks?` | `(continuation-marks? x)` | Returns `#t` if `x` is a continuation mark set. |\n| `call-with-immediate-continuation-mark` | `(call-with-immediate-continuation-mark key default proc)` | Call `proc` with the value of `key` in the immediately enclosing continuation frame, or `default` if no mark for `key` exists. Note the argument order: key, default, proc. |\n\n### Examples\n\n**Basic mark and lookup:**\n\n```scheme\n(import (std misc cont-marks))\n\n(with-continuation-mark 'key 'val\n  (continuation-mark-set->list\n    (current-continuation-marks)\n    'key))\n;; => (val)\n```\n\n**Nested marks accumulate:**\n\n```scheme\n(with-continuation-mark 'depth 0\n  (with-continuation-mark 'depth 1\n    (with-continuation-mark 'depth 2\n      (continuation-mark-set->list\n        (current-continuation-marks)\n        'depth))))\n;; => (2 1 0)\n```\n\n**Tail-call mark replacement:**\n\nWhen marks are set in tail position relative to an existing mark on the same\nframe, the old value is replaced rather than accumulated:\n\n```scheme\n(with-continuation-mark 'k 'a\n  (with-continuation-mark 'k 'b  ;; replaces 'a on the same frame\n    (continuation-mark-set-first #f 'k)))\n;; => b\n```\n\n**Implementing a call stack trace:**\n\n```scheme\n(define (traced name thunk)\n  (with-continuation-mark 'trace name\n    (thunk)))\n\n(define (current-trace)\n  (continuation-mark-set->list\n    (current-continuation-marks)\n    'trace))\n\n(traced 'foo\n  (lambda ()\n    (traced 'bar\n      (lambda ()\n        (current-trace)))))\n;; => (bar foo)\n```\n\n**Immediate continuation mark:**\n\n```scheme\n(with-continuation-mark 'ctx \"request-123\"\n  (call-with-immediate-continuation-mark 'ctx #f\n    (lambda (v) v)))\n;; => \"request-123\"\n```\n\n---\n\n## Non-deterministic Backtracking\n\n**Module:** `(std misc amb)`\n**File:** `lib/std/misc/amb.sls`\n\n```scheme\n(import (std misc amb))\n```\n\n### Overview\n\nThe `amb` operator implements McCarthy's ambiguous choice, enabling\nnon-deterministic programming with automatic backtracking. `amb` picks one of\nits alternatives; if a later assertion fails, execution backtracks to the most\nrecent `amb` and tries the next alternative. This is useful for constraint\nsolving, search problems, and logic programming.\n\n### API Reference\n\n| Procedure / Syntax | Signature | Description |\n|--------------------|-----------|-------------|\n| `amb` | `(amb expr ...)` | Choose one of the expressions. If the current choice leads to failure, backtrack and try the next. `(amb)` with no arguments is equivalent to `(amb-fail)`. `(amb x)` with a single argument returns `x` directly. |\n| `amb-fail` | `(amb-fail)` | Explicitly trigger backtracking. Raises an error if there are no remaining choice points. |\n| `amb-assert` | `(amb-assert condition)` | If `condition` is `#f`, call `(amb-fail)` to backtrack. |\n| `with-amb` | `(with-amb body ...)` | Run an amb computation. Returns the first successful result, or `#f` if no solution exists. |\n| `amb-collect` | `(amb-collect body ...)` | Run an amb computation and collect all successful results into a list. |\n\n### Examples\n\n**Finding a solution:**\n\n```scheme\n(import (std misc amb))\n\n(with-amb\n  (let ([x (amb 1 2 3 4 5)]\n        [y (amb 1 2 3 4 5)])\n    (amb-assert (= (+ x y) 7))\n    (cons x y)))\n;; => (2 . 5)\n```\n\n**Collecting all solutions:**\n\n```scheme\n(amb-collect\n  (let ([x (amb 1 2 3 4 5)]\n        [y (amb 1 2 3 4 5)])\n    (amb-assert (= (+ x y) 6))\n    (cons x y)))\n;; => ((1 . 5) (2 . 4) (3 . 3) (4 . 2) (5 . 1))\n```\n\n**No solution returns `#f`:**\n\n```scheme\n(with-amb\n  (let ([x (amb 1 2 3)])\n    (amb-assert (> x 10))\n    x))\n;; => #f\n```\n\n**Pythagorean triples:**\n\n```scheme\n(define (iota-from a b)\n  ;; Returns list (a a+1 ... b)\n  (if (> a b) '() (cons a (iota-from (+ a 1) b))))\n\n(amb-collect\n  (let* ([a (apply amb (iota-from 1 20))]\n         [b (apply amb (iota-from a 20))]\n         [c (apply amb (iota-from b 20))])\n    (amb-assert (= (+ (* a a) (* b b)) (* c c)))\n    (list a b c)))\n;; => ((3 4 5) (5 12 13) (6 8 10) (8 15 17) (9 12 15) (12 16 20))\n```\n\n**Map coloring (constraint satisfaction):**\n\n```scheme\n(with-amb\n  (let ([wa (amb 'red 'green 'blue)]\n        [nt (amb 'red 'green 'blue)]\n        [sa (amb 'red 'green 'blue)]\n        [q  (amb 'red 'green 'blue)]\n        [nsw (amb 'red 'green 'blue)]\n        [v  (amb 'red 'green 'blue)]\n        [t  (amb 'red 'green 'blue)])\n    ;; Adjacent regions must differ\n    (amb-assert (not (eq? wa nt)))\n    (amb-assert (not (eq? wa sa)))\n    (amb-assert (not (eq? nt sa)))\n    (amb-assert (not (eq? nt q)))\n    (amb-assert (not (eq? sa q)))\n    (amb-assert (not (eq? sa nsw)))\n    (amb-assert (not (eq? sa v)))\n    (amb-assert (not (eq? q nsw)))\n    (amb-assert (not (eq? nsw v)))\n    (list (cons 'WA wa) (cons 'NT nt) (cons 'SA sa)\n          (cons 'Q q) (cons 'NSW nsw) (cons 'V v) (cons 'T t))))\n;; => ((WA . red) (NT . green) (SA . blue) (Q . red) (NSW . red) (V . green) (T . red))\n```\n"}
 {"text": "<!-- FILE: docs/reader-syntax.md -->\n# Jerboa Reader Syntax\n\n_Source of truth: `lib/jerboa/reader.sls`. Last audited 2026-04-22._\n\nJerboa extends Chez's reader with Gerbil-style and Clojure-style surface\nsyntax. **Which reader runs matters**:\n\n| Entry path                                 | Reader used        | `[...]` means          |\n|--------------------------------------------|--------------------|------------------------|\n| `scheme --libdirs lib --script file.ss`    | Chez built-in      | same as `(...)`        |\n| Code read via `(jerboa-read port)` / embed | Jerboa custom      | `(list ...)` wrapper   |\n| `.ss` file starting with `#!cloj`          | Jerboa, cloj mode  | `(list ...)` wrapper   |\n\n**If you are writing everyday `.ss` files and running them with `scheme\n--script`, brackets are just parens** \u2014 use them as block delimiters\nexactly like Gerbil. The Clojure-vector interpretation only activates\nthrough the explicit `jerboa-read` entry point (used by sandboxed embed\nand tooling that re-reads source).\n\nThe table below lists every extension the Jerboa reader adds on top of\nChez. All of these are available when `jerboa-read` is active. A \"mode\"\ncolumn shows `default` (always), `cloj` (only after `#!cloj` or\n`(reader-cloj-mode #t)`), or `both`.\n\n## Extensions\n\n| Syntax                 | Desugars to                                    | Mode    | Example                                          |\n|------------------------|------------------------------------------------|---------|--------------------------------------------------|\n| `[a b c]`              | `(list a b c)`                                 | both\u2020   | `[1 2 3]` \u2192 `(list 1 2 3)`                       |\n| `{m obj a ...}`        | `(~ obj 'm a ...)`                             | default | `{push! stk 1}` \u2192 `(~ stk 'push! 1)`             |\n| `{k v k v ...}`        | `(plist->hash-table (list k v ...))`           | cloj    | `{:a 1 :b 2}` \u2192 hash-map literal                 |\n| `{}` (empty)           | `(make-hash-table)`                            | cloj    | empty hash map                                   |\n| `:pkg/mod`             | `(pkg mod)`                                    | default | `:std/sort` \u2192 `(std sort)`                       |\n| `:pkg/sub/mod`         | `(pkg sub mod)`                                | default | `:std/text/json` \u2192 `(std text json)`             |\n| `:name`                | keyword `#:name`                               | cloj    | `:foo` \u2192 keyword                                 |\n| `name:`                | keyword `#:name`                               | both    | `port: 8080` \u2192 `#:port 8080`                     |\n| `#{a b c}`             | `(hash-set a b c)`                             | both    | `#{1 2 3}` \u2192 set literal                         |\n| `#(a b c)`             | vector `#(a b c)`                              | default | standard vector                                  |\n| `#(+ % 1)`             | `(fn-literal (+ % 1))`                         | cloj    | anonymous fn \u2014 `%` / `%1` / `%2` are args        |\n| `#<<END\\n...\\nEND`     | string literal (heredoc)                       | both    | multi-line raw text, newlines preserved          |\n| `#r\"a\\b\"`              | string `\"a\\\\b\"` (raw \u2014 only `\\\"` escapes)      | both    | useful for regex patterns                        |\n| `@expr`                | `(deref expr)`                                 | both    | `@atom` \u2192 `(deref atom)`                         |\n| `'x` `` `x `` `,x` `,@x` | quote / quasiquote / unquote / -splicing     | both    | standard Scheme                                  |\n| `#!cloj` (top of file) | activates Clojure reader mode for the file     | marker  | affects remaining reads                          |\n| `nil`                  | `#f`                                           | cloj    | literal identifier becomes false                 |\n| `true` / `false`       | `#t` / `#f`                                    | cloj    | literal identifiers                              |\n\n\u2020 Brackets are parens (`=` `(...)`) under Chez's built-in reader, which\nis what runs `.ss` scripts invoked with `scheme --script`. Only the\nJerboa custom reader wraps brackets with `list`.\n\n## What the reader does **not** provide\n\nThe following look like they could be reader syntax but are **plain\nidentifiers** provided by the prelude \u2014 they are NOT reader rewrites:\n\n- `->` `->>` `as->` `some->` `cond->` \u2014 threading macros (syntactic\n  forms, handled by the expander not the reader)\n- `p.x` dot-access \u2014 handled by the `using` form, not the reader\n- `:expr pred?` type casts \u2014 a two-argument form `(: e p)` expanded by\n  the prelude; `:` is a normal identifier\n\nBecause these are expander/macro features, you can rebind or shadow\nthem; reader-level syntax cannot be shadowed.\n\n## Safety limits\n\nThe Jerboa reader enforces resource ceilings (configurable via\nparameters in `(jerboa reader)`):\n\n- `(*max-nesting-depth*)` \u2014 how deep forms can nest\n- `(*max-list-length*)` \u2014 elements per list\n- `(*max-symbol-length*)` \u2014 chars per symbol\n- `(*max-comment-nesting*)` \u2014 depth of `#| ... |#` block comments\n\nThese prevent untrusted input from exhausting resources during parse.\nChez's built-in reader has no such limits, so any `.ss` file loaded\nthrough stock `scheme` bypasses them.\n\n## Quick reference for LLMs\n\nIf you are generating Jerboa code:\n\n1. **Default assumption**: the user runs code with `scheme --script`,\n   so brackets behave as parens. Write `(let ([x 1]) body)` freely.\n2. **Method dispatch**: `{m obj arg ...}` always means `(~ obj 'm\n   arg ...)`. Use when you know `obj` has a method `m`.\n3. **Module paths**: prefer the explicit `(import (std text json))`\n   form in examples; `:std/text/json` is correct but looks unusual\n   outside of Gerbil-style code.\n4. **Heredocs**: `#<<END\\nmulti\\nline\\nEND` is the only way to embed\n   a string containing unescaped quotes and newlines.\n5. **Never** write `(fn-literal ...)` or `(deref ...)` directly \u2014\n   prefer `(lambda (...) ...)` and `(@ atom)` spellings for clarity\n   unless you're targeting Clojure-mode code.\n"}
 {"text": "<!-- FILE: docs/safety-guide.md -->\n# Jerboa Safety Guide\n\nPractical guide for writing secure Jerboa applications. Covers all safety\nfeatures as of 2026-03-21.\n\n---\n\n## 1. Quick Start: The Safe Prelude\n\nThe single most important line in any Jerboa application:\n\n```scheme\n(import (jerboa prelude safe))\n```\n\nThis gives you the full Jerboa standard library with safety turned on by\ndefault. Specifically, you get:\n\n- **Contract-checked stdlib** \u2014 SQLite, TCP, file I/O, and JSON functions\n  validate arguments before FFI calls and return structured error conditions\n- **Resource management** \u2014 `with-resource` for RAII-style cleanup\n- **Structured concurrency** \u2014 `with-task-scope` instead of raw `fork-thread`\n- **Timeouts** \u2014 `with-timeout` and a configurable `*default-timeout*`\n- **Safe serialization** \u2014 `safe-fasl-read`/`safe-fasl-write` that reject\n  procedures and enforce size limits\n- **Sandbox entry point** \u2014 `run-safe` and `run-safe-eval` for untrusted code\n- **Error conditions** \u2014 structured hierarchy instead of bare `(error ...)`\n\nWhat you do NOT get (intentionally):\n\n- `foreign-procedure`, `c-lambda`, `begin-foreign` \u2014 no raw FFI\n- `fork-thread` \u2014 use `with-task-scope` and `scope-spawn` instead\n- `eval` \u2014 use `run-safe-eval` for untrusted input\n\nIf you need raw FFI or unscoped threads, import `(jerboa prelude)` instead.\nBut the safe prelude is the recommended default for application code.\n\n---\n\n## 2. The `run-safe` Sandbox\n\n`run-safe` is the one-call entry point for running untrusted code with all\nprotections applied. It forks a child process, applies irreversible kernel\nprotections, runs your code, and returns the result to the parent.\n\n### Basic Usage\n\n```scheme\n(import (jerboa prelude safe))\n\n;; Run a thunk with default protections:\n;; - 30 second timeout\n;; - compute-only seccomp filter (blocks network, filesystem writes)\n;; - No Landlock rules (add your own for filesystem restriction)\n(run-safe (lambda () (+ 1 2)))  ;; => 3\n\n;; Evaluate a string in a restricted environment (113-binding allowlist):\n(run-safe-eval \"(map (lambda (x) (* x x)) '(1 2 3))\")  ;; => (1 4 9)\n```\n\n### Custom Configuration\n\n```scheme\n;; Allow I/O but restrict to 10 seconds:\n(run-safe (lambda () (process-data input))\n  (make-sandbox-config\n    'timeout 10\n    'seccomp 'io-only))\n\n;; Filesystem sandbox: read-only access to specific directories:\n(run-safe (lambda () (read-config))\n  (make-sandbox-config\n    'landlock (make-readonly-ruleset \"/etc\" \"/usr/lib\")\n    'seccomp 'io-only\n    'timeout 5))\n\n;; No timeout (for trusted but resource-limited code):\n(run-safe (lambda () (long-computation))\n  (make-sandbox-config 'timeout #f 'seccomp 'compute-only))\n```\n\n### Config Options\n\n| Key | Values | Default | Effect |\n|-----|--------|---------|--------|\n| `timeout` | seconds or `#f` | 30 | Engine-based preemptive timeout |\n| `seccomp` | `'compute-only`, `'io-only`, `'network-server`, filter, `#f` | `'compute-only` | Syscall filter (irreversible) |\n| `landlock` | ruleset or `#f` | `#f` | Filesystem access restriction (irreversible) |\n| `capabilities` | list or `'()` | `'()` | Runtime capability tokens |\n\n### Default Parameters\n\nYou can set global defaults instead of passing config every time:\n\n```scheme\n(parameterize ([*sandbox-timeout* 10]\n               [*sandbox-seccomp* 'io-only])\n  (run-safe (lambda () (do-work))))\n```\n\n### What It Protects Against\n\n- **Code injection**: `run-safe-eval` uses a 113-binding allowlist. `system`,\n  `eval`, `load`, `foreign-procedure` are not in the allowlist. No amount of\n  clever input can summon them.\n- **Filesystem escape**: Landlock restricts filesystem access at the kernel\n  level. Once installed, it cannot be undone from Scheme or C.\n- **Syscall abuse**: Seccomp BPF filters are kernel-enforced. Even arbitrary\n  code execution inside the sandbox cannot bypass them.\n- **Infinite loops**: Engine-based timeout preempts runaway Scheme code.\n- **Parent compromise**: All protections are applied in a forked child. The\n  parent process remains unrestricted.\n\n### What It Does NOT Protect Against\n\n- **Memory exhaustion**: A `(make-bytevector 1000000000)` inside the sandbox\n  can OOM the child before the timeout fires. There is no `setrlimit` yet.\n  For DoS protection, run sandboxed code in a resource-limited container.\n- **Blocked FFI calls**: The engine timeout preempts Scheme code but cannot\n  interrupt a blocking C/Rust FFI call. Use socket-level timeouts\n  (`SO_RCVTIMEO`) for network operations.\n- **Temp file race**: The child communicates results via `/tmp/jerboa-sandbox-*`\n  with a random suffix. A local attacker with `/tmp` access could potentially\n  interfere. This is a known issue.\n\n---\n\n## 3. Resource Management\n\n### `with-resource` \u2014 Guaranteed Cleanup\n\n`with-resource` guarantees that resources are cleaned up even when exceptions\noccur. It uses `dynamic-wind` for LIFO cleanup ordering.\n\n```scheme\n(import (jerboa prelude safe))\n\n;; Multiple resources \u2014 cleaned up in reverse order on any exit:\n(with-resource ([db (sqlite-open \"app.db\")]\n                [sock (tcp-connect \"api.example.com\" 443)]\n                [f (open-safe-input-file \"config.txt\")])\n  (let ([config (read f)])\n    (sqlite-exec db (format \"INSERT INTO log(msg) VALUES('~a')\" \"started\"))\n    (tcp-write-string sock \"GET / HTTP/1.1\\r\\n\\r\\n\")))\n;; db, sock, and f are ALL guaranteed closed here\n\n;; Single resource shorthand:\n(with-resource1 (db (sqlite-open \"test.db\"))\n  (sqlite-query db \"SELECT 1\"))\n```\n\n### Auto-Detection\n\n`with-resource` auto-detects cleanup procedures for common types:\n\n- **Ports** (files, string ports) \u2014 calls `close-port` (flushes output first)\n- **Registered types** \u2014 anything registered via `register-resource-cleanup!`\n\nFor other types, provide an explicit cleanup:\n\n```scheme\n(with-resource ([buf (make-bytevector 4096) (lambda (b) (bytevector-fill! b 0))])\n  (fill-buffer buf))\n```\n\n### Guardian Safety Net\n\nIf you forget `with-resource`, the guardian-based finalizer in `(std safe)`\ncatches resource handles that are garbage collected without being closed. It\nlogs a warning to `current-error-port` and attempts best-effort cleanup.\n\nThis is a safety net, not a strategy. Always use `with-resource` explicitly.\n\nTo check for leaked resources at shutdown:\n\n```scheme\n(poll-resource-finalizers!)\n```\n\n---\n\n## 4. Contract-Checked Standard Library\n\nWhen you import `(jerboa prelude safe)`, the standard names (`sqlite-exec`,\n`tcp-connect`, etc.) are transparently replaced with contract-checked\nversions from `(std safe)`.\n\n### What the Contracts Check\n\n- **Type validation** before FFI calls \u2014 e.g., `sqlite-exec` verifies the\n  database handle and SQL string before calling into C\n- **Post-condition checks** on results\n- **SQL injection heuristics** \u2014 multi-statement detection, comment injection,\n  and other patterns are rejected at runtime\n\n```scheme\n(import (jerboa prelude safe))\n\n;; This works \u2014 single statement, no injection patterns:\n(sqlite-exec db \"SELECT * FROM users WHERE id = ?\")\n\n;; This is rejected \u2014 multi-statement detected:\n(sqlite-exec db \"SELECT 1; DROP TABLE users\")\n;; => raises &db-query-error with SQL injection warning\n```\n\n### SQL Injection Detection Limits\n\nThe runtime SQL check is a heuristic speed bump, not a wall. It catches\ncommon patterns (`;` statement separators, `--` comments, `/* */` blocks)\nbut a determined adversary can craft bypasses. **Always use parameterized\nqueries as your primary defense**:\n\n```scheme\n;; GOOD \u2014 parameterized:\n(sqlite-query db \"SELECT * FROM users WHERE id = ?\" user-id)\n\n;; BAD \u2014 string interpolation (the lint rule will also warn):\n(sqlite-query db (string-append \"SELECT * FROM users WHERE id = \" user-id))\n```\n\n### Release Mode\n\nFor production code that has been tested, you can skip contract checks for\nzero overhead:\n\n```scheme\n(parameterize ([*safe-mode* 'release])\n  (sqlite-exec db \"SELECT 1\"))  ;; no pre/post-condition checks\n```\n\nDefault is `'check`. Only use `'release` after thorough testing.\n\n---\n\n## 5. Structured Concurrency\n\nThe safe prelude exports structured concurrency primitives and intentionally\ndoes NOT export `fork-thread`.\n\n### `with-task-scope`\n\nAll spawned tasks are guaranteed terminated when the scope exits. No task\noutlives its scope.\n\n```scheme\n(import (jerboa prelude safe))\n\n;; Spawn parallel work \u2014 both tasks are cancelled if scope exits early:\n(with-task-scope\n  (let ([t1 (scope-spawn (lambda () (fetch-url \"https://api.example.com/a\")))]\n        [t2 (scope-spawn (lambda () (query-db \"SELECT count(*) FROM users\")))])\n    (values (task-await t1) (task-await t2))))\n\n;; Named tasks (for debugging):\n(with-task-scope\n  (let ([t (scope-spawn-named \"data-loader\" (lambda () (load-data)))])\n    (task-await t)))\n```\n\n### `parallel` and `race`\n\nConvenience forms for common patterns:\n\n```scheme\n;; Run all, return all results:\n(parallel\n  (lambda () (fetch-users))\n  (lambda () (fetch-orders))\n  (lambda () (fetch-config)))\n\n;; Run all, return first to complete, cancel the rest:\n(race\n  (lambda () (query-primary-db \"SELECT 1\"))\n  (lambda () (query-replica-db \"SELECT 1\")))\n```\n\n### Task Control\n\n```scheme\n(with-task-scope\n  (let ([t (scope-spawn (lambda () (slow-computation)))])\n    ;; Check if done without blocking:\n    (task-done? t)    ;; => #f\n\n    ;; Cancel:\n    (task-cancel t)\n\n    ;; Block until result (or re-raise task exception):\n    (task-await t)))\n```\n\n### Why Not `fork-thread`?\n\nRaw `fork-thread` creates threads that outlive their calling context. This\nleads to:\n\n- Resource leaks when threads hold references to closed handles\n- Uncatchable exceptions in threads that nobody joins\n- Race conditions that are hard to reproduce\n\n`with-task-scope` prevents all of these by construction. If you genuinely\nneed raw threads, import `(jerboa prelude)` instead \u2014 but you lose the\nstructured cleanup guarantees.\n\n---\n\n## 6. Timeouts\n\n### `with-timeout`\n\nUses Chez Scheme's engine system for true preemptive timeout. Works even if\nthe code is stuck in a Scheme-level infinite loop.\n\n```scheme\n(import (jerboa prelude safe))\n\n;; Timeout after 5 seconds:\n(with-timeout 5\n  (tcp-read sock 1024))\n\n;; Raises &jerboa-timeout on expiry.\n;; Handle it:\n(guard (exn\n        [(timeout-error? exn) (displayln \"operation timed out\")])\n  (with-timeout 2\n    (infinite-loop)))\n```\n\n### `*default-timeout*`\n\nControls the default timeout for safe-* operations. Default is 30 seconds.\n\n```scheme\n;; Set a 10-second default for this scope:\n(parameterize ([*default-timeout* 10])\n  ;; All safe operations in this dynamic extent use 10s timeout\n  (tcp-read sock 1024))\n\n;; Disable timeouts (not recommended for untrusted input):\n(parameterize ([*default-timeout* #f])\n  (long-trusted-computation))\n```\n\n### Limitations\n\nThe engine-based timeout cannot interrupt blocked foreign calls (C/Rust FFI).\nIf a C function blocks on a socket read, the engine timer has no effect until\ncontrol returns to Scheme. For network I/O, set socket-level timeouts\n(`SO_RCVTIMEO`/`SO_SNDTIMEO`) as a complementary measure.\n\n---\n\n## 7. Type Checking\n\n### `define/ct` \u2014 Compile-Time Checked Functions\n\n```scheme\n(import (std typed check))\n\n(define/ct (add [x : integer] [y : integer]) : integer\n  (+ x y))\n\n(define/ct (greet [name : string]) : string\n  (string-append \"Hello, \" name))\n```\n\nType mismatches are caught at compile time (macro expansion time). By default\nthey are warnings. For strict enforcement:\n\n```scheme\n;; Make type mismatches fatal compilation errors:\n(parameterize ([*type-errors-fatal* #t])\n  (compile-file \"my-module.ss\"))\n```\n\n### `lambda/ct` \u2014 Typed Lambdas\n\n```scheme\n(define handler\n  (lambda/ct ([request : string] [timeout : integer]) : string\n    (process request timeout)))\n```\n\n### Checking Existing Code\n\n```scheme\n;; Type-check a file and get error reports:\n(type-check-file \"my-module.ss\")\n;; => list of type-error records\n\n;; Type-check forms programmatically:\n(check-program-types\n  '((define (f x) (+ x \"hello\"))))  ;; => type mismatch: integer vs string\n```\n\n### Coverage Limitations\n\nThe type system is gradual and opt-in. Only functions annotated with\n`define/ct` or `lambda/ct` are checked. Calls from checked code to unchecked\ncode cross an unchecked boundary \u2014 type confusion bugs can still occur there.\nThis is standard for gradual type systems.\n\n---\n\n## 8. Kernel Sandboxing\n\n### Landlock \u2014 Filesystem Restriction\n\nLandlock restricts filesystem access without root. Rules are irreversible.\nAvailable on Linux 5.13+.\n\n```scheme\n(import (std security landlock))\n\n;; Build a ruleset:\n(let ([rules (make-landlock-ruleset)])\n  ;; Grant read-only access to specific directories:\n  (landlock-add-read-only! rules \"/usr/lib\")\n  (landlock-add-read-only! rules \"/etc\")\n  ;; Grant read-write to a working directory:\n  (landlock-add-read-write! rules \"/tmp/myapp\")\n  ;; Grant execute permission:\n  (landlock-add-execute! rules \"/usr/bin\")\n  ;; Install (IRREVERSIBLE \u2014 cannot undo):\n  (landlock-install! rules))\n;; All filesystem access outside these paths is now blocked\n\n;; Convenience: scope-based Landlock\n(with-landlock (make-readonly-ruleset \"/usr/lib\" \"/etc\")\n  (read-config-files))\n\n;; Pre-built rulesets:\n(make-readonly-ruleset \"/path1\" \"/path2\" ...)\n(make-tmpdir-ruleset \"/tmp/myapp\")\n\n;; Check availability:\n(landlock-available?)  ;; => #t on Linux 5.13+ x86_64\n```\n\n### Seccomp \u2014 Syscall Filtering\n\nSeccomp BPF restricts which system calls a process can make. Filters are\nirreversible and kernel-enforced. Available on Linux x86_64.\n\n```scheme\n(import (std security seccomp))\n\n;; Pre-built filters:\n(seccomp-install! (compute-only-filter))    ;; CPU-only: blocks network, FS writes\n(seccomp-install! (io-only-filter))         ;; Allow file I/O, block network\n(seccomp-install! (network-server-filter))  ;; Allow network, block exec\n\n;; Custom filter:\n(let ([filter (make-seccomp-filter\n                'default-action (seccomp-errno 1)  ;; EPERM for unmatched\n                'allowed '(read write exit-group brk mmap mprotect))])\n  (seccomp-install! filter))\n\n;; Check availability:\n(seccomp-available?)  ;; => #t on Linux x86_64\n```\n\n### When to Use Directly vs. via `run-safe`\n\nUse `run-safe` when:\n- You want all protections applied together (timeout + seccomp + Landlock)\n- You need fork-based isolation (protections apply only to child)\n- You are running untrusted code\n\nUse Landlock/seccomp directly when:\n- You want to restrict the main process permanently (e.g., a server that\n  drops privileges after startup)\n- You need fine-grained control over the protection order\n- You want irreversible self-restriction as a defense-in-depth measure\n\nRemember: **both Landlock and seccomp are irreversible**. Once installed in a\nprocess, they cannot be removed \u2014 only tightened further. `run-safe` handles\nthis by forking a child, so the parent stays unrestricted.\n\n---\n\n## 9. Error Handling\n\n### Structured Condition Hierarchy\n\nAll Jerboa errors inherit from `&jerboa`, which inherits from R6RS `&serious`.\nUse `guard` to catch specific error kinds:\n\n```scheme\n(import (jerboa prelude safe))\n\n(guard (exn\n        [(db-query-error? exn)\n         (displayln \"bad query\")]\n        [(connection-refused? exn)\n         (displayln \"server down\")]\n        [(timeout-error? exn)\n         (displayln \"timed out\")]\n        [(resource-leak? exn)\n         (displayln \"resource leaked\")]\n        [(jerboa-condition? exn)\n         (displayln (format \"jerboa error in ~a\"\n                            (jerboa-condition-subsystem exn)))])\n  (do-work))\n```\n\n### Full Hierarchy\n\n```\n&jerboa (root)\n  &jerboa-network\n    &connection-refused\n    &connection-timeout\n    &dns-failure\n    &tls-error\n    &network-read-error\n    &network-write-error\n  &jerboa-db\n    &db-connection-error\n    &db-query-error\n    &db-constraint-violation\n    &db-timeout\n  &jerboa-actor\n    &actor-dead\n    &mailbox-full\n    &supervision-failure\n    &actor-timeout\n  &jerboa-resource\n    &resource-leak\n    &resource-already-closed\n    &resource-exhausted\n  &jerboa-timeout\n  &jerboa-serialization\n    &unsafe-deserialize\n    &serialize-size-exceeded\n  &jerboa-parse\n    &parse-depth-exceeded\n    &parse-size-exceeded\n    &parse-invalid-input\n```\n\n### Error Context\n\n`(std error context)` provides `with-context` for accumulating diagnostic\ncontext through call chains:\n\n```scheme\n(import (std error context))\n\n(with-context \"loading user profile\"\n  (with-context (format \"user-id: ~a\" user-id)\n    (load-profile user-id)))\n;; If load-profile raises, the error includes both context strings\n```\n\n---\n\n## 10. Safe Serialization\n\n### `safe-fasl-read` / `safe-fasl-write`\n\nWraps Chez FASL with safety checks for untrusted data:\n\n```scheme\n(import (jerboa prelude safe))\n\n;; Write (rejects procedures by default):\n(call-with-output-file \"data.fasl\"\n  (lambda (port) (safe-fasl-write '(1 2 3) port)))\n\n;; Read (rejects procedures and unregistered record types):\n(call-with-input-file \"data.fasl\"\n  (lambda (port) (safe-fasl-read port)))\n\n;; Bytevector convenience:\n(let ([bytes (safe-fasl-write-bytevector '(1 2 3))])\n  (safe-fasl-read-bytevector bytes))\n```\n\n### Configuration\n\n```scheme\n;; Allow procedures (for trusted local persistence only):\n(parameterize ([*fasl-allow-procedures* #t])\n  (safe-fasl-write my-closure port))\n\n;; Size limits:\n(parameterize ([*fasl-max-object-count* 10000]     ;; max 10K objects (default 1M)\n               [*fasl-max-byte-size* (* 1 1024 1024)])  ;; max 1MB (default 100MB)\n  (safe-fasl-read port))\n```\n\n### Record Type Registry\n\nOnly explicitly registered record types can be deserialized:\n\n```scheme\n(define-record-type user (fields name email))\n\n;; Register for deserialization:\n(register-safe-record-type! (record-type-descriptor user))\n\n;; Now safe-fasl-read will accept user records.\n;; Unregister when no longer needed:\n(unregister-safe-record-type! (record-type-descriptor user))\n```\n\n### Decompression Bomb Limits\n\nThese limits apply across the safe APIs:\n\n| Parser | Parameter | Default |\n|--------|-----------|---------|\n| JSON | `*json-max-depth*` | 512 |\n| JSON | `*json-max-string-length*` | 10 MB |\n| XML/SXML | `*sxml-max-depth*` | 512 |\n| XML/SXML | `*sxml-max-output-size*` | 50 MB |\n| FASL | `*fasl-max-object-count*` | 1,000,000 |\n| FASL | `*fasl-max-byte-size*` | 100 MB |\n| Zlib | (hardcoded in Rust backend) | 100 MB |\n\n---\n\n## 11. Linting\n\n`(std lint)` provides 14 static analysis rules. Run them on your source files\nto catch common issues before they become bugs.\n\n### Usage\n\n```scheme\n(import (std lint))\n\n;; Lint a file:\n(let ([results (lint-file default-linter \"my-module.ss\")])\n  (for-each\n    (lambda (r)\n      (displayln (format \"[~a] ~a: ~a\"\n                         (lint-result-severity r)\n                         (lint-result-rule r)\n                         (lint-result-message r))))\n    results))\n\n;; Lint a string:\n(lint-string default-linter \"(import (std db sqlite-native)) (define car 42)\")\n\n;; Summary counts:\n(lint-summary results)  ;; => ((error . 0) (warn . 2) (info . 1))\n```\n\n### The 14 Rules\n\n**Safety rules** (severity: warn):\n\n| Rule | Detects |\n|------|---------|\n| `unsafe-import` | Importing raw FFI modules when safe alternatives exist |\n| `sql-interpolation` | Building SQL via `string-append`/`format` inside sqlite calls |\n| `redefine-builtin` | Redefining `car`, `map`, `+`, etc. |\n| `duplicate-import` | Importing the same module more than once |\n\n**Safety rules** (severity: info):\n\n| Rule | Detects |\n|------|---------|\n| `bare-error` | Using `(error ...)` instead of structured conditions |\n| `unused-only-import` | Symbols imported via `(only ...)` that are never used |\n\n**Hygiene rules** (severity: warn/info):\n\n| Rule | Detects |\n|------|---------|\n| `shadowed-define` | Bindings that shadow outer bindings |\n| `unused-define` | Top-level definitions never referenced in the same file |\n\n**Style rules** (severity: info):\n\n| Rule | Detects |\n|------|---------|\n| `empty-begin` | `(begin)` with no forms |\n| `single-arm-cond` | `cond` with only one clause (use `if` instead) |\n| `missing-else` | `if` without an else branch |\n| `deep-nesting` | Expressions nested more than 10 levels |\n| `long-lambda` | Lambda bodies with more than 20 forms |\n| `magic-number` | Numeric literals > 100 (suggest named constants) |\n\n### Custom Rules\n\n```scheme\n(let ([linter (make-linter)])\n  ;; Add a custom rule:\n  (add-rule! linter 'no-display\n    (lambda (forms)\n      (filter-map\n        (lambda (f)\n          (and (pair? f) (eq? (car f) 'display)\n               (make-lint-result #f #f #f 'warn\n                 \"use displayln instead of display\" 'no-display)))\n        forms)))\n\n  ;; Remove a built-in rule:\n  (remove-rule! linter 'magic-number)\n\n  (lint-file linter \"my-module.ss\"))\n```\n\n---\n\n## 12. Known Limitations\n\nBe honest about what Jerboa does NOT protect against:\n\n### No Memory Limits in Sandbox\n\n`run-safe` does not set `setrlimit(RLIMIT_AS)` in the child process. A\nmalicious thunk can exhaust memory before the timeout fires. If you run\nuntrusted code, use OS-level memory limits (cgroups, containers) as a\ncomplementary measure.\n\n### Kernel Protections Are Linux x86_64 Only\n\nLandlock requires Linux 5.13+. Seccomp BPF uses x86_64-specific syscall\nnumbers. On other platforms (macOS, ARM Linux, BSDs), these protections\nsilently degrade \u2014 the code runs without kernel sandboxing. The Scheme-level\nallowlist and timeout still apply.\n\n### Temp File Race in `run-safe`\n\n`run-safe` communicates results via `/tmp/jerboa-sandbox-<random>`. A local\nattacker with `/tmp` access could create symlinks to redirect the result\nfile. This should use `mkstemp` or `O_TMPFILE` but does not yet.\n\n### Engine Timeout Cannot Interrupt FFI\n\nThe Chez engine timeout preempts Scheme computation but not blocked foreign\ncalls. If your code calls a C function that blocks on a socket, the timeout\nhas no effect until that call returns. Set socket-level timeouts separately.\n\n### Type System Is Gradual\n\nOnly `define/ct` and `lambda/ct` annotated functions are type-checked. Calls\nfrom checked code into unchecked code cross an unchecked boundary. Type\nconfusion bugs can occur at these boundaries.\n\n### No Race Detector\n\nThere is no dynamic race detection for concurrent code. Structured concurrency\nvia `with-task-scope` reduces risk but does not eliminate data races. Code\nusing raw `fork-thread` (from the non-safe prelude) has no race detection.\n\n### Deadlock Detection Is Opt-In\n\n`(std concur deadlock)` provides `make-checked-mutex` and\n`with-checked-mutex` for runtime deadlock detection via wait-for graph\nanalysis. This is not automatic \u2014 you must explicitly use checked mutexes.\nRegular `make-mutex` has no deadlock detection.\n\n### SQL Injection Heuristic Is Bypassable\n\nThe runtime SQL injection check in `(std safe)` uses pattern matching. It\ncatches common attacks but a determined adversary with knowledge of the\nheuristics can craft bypasses. Parameterized queries are the real defense.\n\n### Import Conflict in Safe Prelude\n\nThe safe prelude has overlapping symbol exports between imported modules that\nproduce a \"multiple definitions\" warning. This is cosmetic but means symbol\nresolution order could theoretically surprise you in edge cases. In practice,\nthe safe version always wins because it is bound last.\n\n### Build Reproducibility Is Partial\n\n`(std build reproducible)` provides content-addressed artifacts and SHA-256\nhashing, but bit-identical reproducible builds end-to-end are not yet\nverified. The SBOM detects Scheme, C, and Rust dependencies but full build\norchestration is not complete.\n\n---\n\n## Recommended Patterns\n\n### Server That Drops Privileges\n\n```scheme\n(import (jerboa prelude safe))\n(import (std security landlock))\n(import (std security seccomp))\n\n;; Phase 1: Start up with full privileges\n(define config (read-config \"/etc/myapp/config.scm\"))\n(define db (sqlite-open (config-db-path config)))\n(define server (tcp-listen (config-port config)))\n\n;; Phase 2: Drop privileges (irreversible)\n(let ([rules (make-landlock-ruleset)])\n  (landlock-add-read-only! rules \"/etc/myapp\")\n  (landlock-add-read-write! rules \"/var/lib/myapp\")\n  (landlock-install! rules))\n(seccomp-install! (network-server-filter))\n\n;; Phase 3: Serve requests with restricted privileges\n(let loop ()\n  (with-resource ([client (tcp-accept server)])\n    (with-timeout 30\n      (handle-request db client)))\n  (loop))\n```\n\n### Processing Untrusted User Input\n\n```scheme\n(import (jerboa prelude safe))\n\n(define (eval-user-expression user-input)\n  ;; Fork a child, apply all protections, evaluate in 113-binding allowlist:\n  (guard (exn\n          [(sandbox-error? exn)\n           (format \"Error: ~a\" (sandbox-error-detail exn))]\n          [(timeout-error? exn)\n           \"Timed out\"])\n    (run-safe-eval user-input\n      (make-sandbox-config 'timeout 5))))\n```\n\n### Robust Resource Handling\n\n```scheme\n(import (jerboa prelude safe))\n\n(define (transfer-data source-path dest-path)\n  (with-resource ([in (open-safe-input-file source-path)]\n                  [out (open-safe-output-file dest-path)])\n    (with-timeout 60\n      (let loop ()\n        (let ([line (get-line in)])\n          (unless (eof-object? line)\n            (put-string out line)\n            (newline out)\n            (loop)))))))\n;; Both files guaranteed closed, even on timeout or exception\n```\n\n### Parallel Work with Cancellation\n\n```scheme\n(import (jerboa prelude safe))\n\n(define (fetch-with-fallback primary-url fallback-url)\n  ;; First to complete wins, other is cancelled:\n  (race\n    (lambda () (fetch-url primary-url))\n    (lambda () (fetch-url fallback-url))))\n```\n"}
@@ -358,7 +358,7 @@
 {"text": "<!-- FILE: docs/wasm-example.md -->\n# WASM Practical Examples\n\nThis guide shows how to write real programs using Jerboa's WASM pipeline:\ncompile Scheme to WASM bytecode, run it in the Rust wasmi sandbox, and\nintegrate with jsh (Jerboa Shell).\n\nFor architecture and API reference, see [wasm.md](wasm.md).\n\n## Table of Contents\n\n- [Prerequisites](#prerequisites)\n- [Example 1: Numeric Computation](#example-1-numeric-computation)\n- [Example 2: Memory-Based Data Processing](#example-2-memory-based-data-processing)\n- [Example 3: A Byte-Level Tokenizer](#example-3-a-byte-level-tokenizer)\n- [Example 4: Parser with Import Callbacks](#example-4-parser-with-import-callbacks)\n- [Example 5: State Machine Protocol Parser](#example-5-state-machine-protocol-parser)\n- [Compiling to .wasm Files](#compiling-to-wasm-files)\n- [String I/O Conventions](#string-io-conventions)\n- [What Works Well in WASM](#what-works-well-in-wasm)\n- [Current Limitations](#current-limitations)\n- [jsh Integration Roadmap](#jsh-integration-roadmap)\n\n## Prerequisites\n\n```scheme\n(import (except (chezscheme) compile-program)\n        (jerboa wasm format)\n        (jerboa wasm codegen)\n        (jerboa wasm runtime))\n\n;; For Rust wasmi sandbox (optional \u2014 provides ROP isolation):\n(import (std wasm sandbox))\n```\n\nTwo execution modes:\n\n| Mode | Module | Runs in | Use case |\n|------|--------|---------|----------|\n| **Interpreter** | `(jerboa wasm runtime)` | Chez Scheme process | Development, debugging |\n| **Sandbox** | `(std wasm sandbox)` | Rust wasmi (separate address space) | Production, untrusted code |\n\nBoth accept the same bytecode from `compile-program`.\n\n---\n\n## Example 1: Numeric Computation\n\nPure functions on i32/i64/f32/f64 \u2014 the simplest case.\n\n```scheme\n(define bv (compile-program\n  '((define (factorial (n i32) -> i32)\n      (if (= n 0) 1 (* n (factorial (- n 1)))))\n\n    (define (fibonacci (n i32) -> i32)\n      (if (<= n 1) n\n        (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))\n\n    (define (gcd (a i32) (b i32) -> i32)\n      (if (= b 0) a (gcd b (remainder a b))))\n\n    (define (is-prime (n i32) -> i32)\n      (if (<= n 1) 0\n        (let ((i 2) (limit (+ 1 (i32.trunc_f64_s (f64.sqrt (f64.convert_i32_s n))))))\n          (while (and (<= i limit) (!= (remainder n i) 0))\n            (set! i (+ i 1)))\n          (if (> i limit) 1 0)))))))\n\n;; Run in interpreter\n(define rt (make-wasm-runtime))\n(wasm-runtime-load rt bv)\n(wasm-runtime-call rt \"factorial\" 12)   ;; => 479001600\n(wasm-runtime-call rt \"fibonacci\" 20)   ;; => 6765\n(wasm-runtime-call rt \"gcd\" 48 18)      ;; => 6\n(wasm-runtime-call rt \"is-prime\" 997)   ;; => 1\n\n;; Run in Rust wasmi sandbox (identical bytecode)\n(define mod-h (wasm-sandbox-load bv))\n(define inst (wasm-sandbox-instantiate mod-h))\n(wasm-sandbox-call inst \"factorial\" 12) ;; => 479001600\n(wasm-sandbox-free inst)\n(wasm-sandbox-free-module mod-h)\n```\n\n---\n\n## Example 2: Memory-Based Data Processing\n\nWASM linear memory is a flat byte array. Use it for arrays, buffers, and\nstructured data.\n\n```scheme\n(define bv (compile-program\n  '((define-memory 1)  ;; 1 page = 64 KiB\n\n    ;; Sum an array of i32 values at [base, base + count*4)\n    (define (sum-array (base i32) (count i32) -> i32)\n      (let ((total 0) (i 0))\n        (while (< i count)\n          (set! total (+ total (i32.load (+ base (* i 4)))))\n          (set! i (+ i 1)))\n        total))\n\n    ;; Find maximum in an i32 array\n    (define (max-array (base i32) (count i32) -> i32)\n      (let ((best (i32.load base)) (i 1))\n        (while (< i count)\n          (let ((val (i32.load (+ base (* i 4)))))\n            (when (> val best)\n              (set! best val)))\n          (set! i (+ i 1)))\n        best))\n\n    ;; In-place bubble sort on an i32 array\n    (define (sort-array (base i32) (count i32) -> i32)\n      (let ((i 0))\n        (while (< i (- count 1))\n          (let ((j 0))\n            (while (< j (- count i 1))\n              (let ((addr-j (+ base (* j 4)))\n                    (addr-j1 (+ base (* (+ j 1) 4))))\n                (let ((a (i32.load addr-j))\n                      (b (i32.load addr-j1)))\n                  (when (> a b)\n                    (i32.store addr-j b)\n                    (i32.store addr-j1 a))))\n              (set! j (+ j 1))))\n          (set! i (+ i 1)))\n        0)))))\n\n;; Use from host\n(define mod-h (wasm-sandbox-load bv))\n(define inst (wasm-sandbox-instantiate mod-h))\n\n;; Write [30, 10, 50, 20, 40] at offset 0 as little-endian i32s\n(wasm-sandbox-memory-write inst 0\n  (u8-list->bytevector\n    '(30 0 0 0  10 0 0 0  50 0 0 0  20 0 0 0  40 0 0 0)))\n\n(wasm-sandbox-call inst \"sum-array\" 0 5)   ;; => 150\n(wasm-sandbox-call inst \"max-array\" 0 5)   ;; => 50\n(wasm-sandbox-call inst \"sort-array\" 0 5)  ;; => 0 (in-place)\n\n;; Read sorted result back\n(wasm-sandbox-memory-read inst 0 20)\n;; => #vu8(10 0 0 0 20 0 0 0 30 0 0 0 40 0 0 0 50 0 0 0)\n\n(wasm-sandbox-free inst)\n(wasm-sandbox-free-module mod-h)\n```\n\n---\n\n## Example 3: A Byte-Level Tokenizer\n\nThis is the core use case: write a lexer in Jerboa's WASM dialect, compile it,\nrun it on untrusted input in the sandbox. The host writes UTF-8 bytes into WASM\nmemory, the WASM tokenizer scans them, and writes token descriptors to a\nseparate memory region that the host reads back.\n\n### Memory layout convention\n\n```\nOffset 0x0000 .. 0x0FFF  (4 KiB)   Input buffer (host writes here)\nOffset 0x1000 .. 0x2FFF  (8 KiB)   Token output: (type:i32, start:i32, end:i32) \u00d7 N\n```\n\n### The tokenizer\n\n```scheme\n(define tokenizer-bytecode\n  (compile-program\n    '((define-memory 1)\n\n      ;; Token output pointer (starts at 0x1000)\n      (define-global out-ptr i32 #t 4096)\n      ;; Token count\n      (define-global token-count i32 #t 0)\n\n      ;; --- Character classes ---\n\n      (define (is-space (c i32) -> i32)\n        (or (= c 32) (= c 9) (= c 10) (= c 13)))\n\n      (define (is-digit (c i32) -> i32)\n        (and (>= c 48) (<= c 57)))\n\n      (define (is-alpha (c i32) -> i32)\n        (or (and (>= c 65) (<= c 90))    ;; A-Z\n            (and (>= c 97) (<= c 122))   ;; a-z\n            (= c 95)))                     ;; _\n\n      (define (is-alnum (c i32) -> i32)\n        (or (is-alpha c) (is-digit c)))\n\n      ;; --- Token emitter ---\n      ;; Token types: 0=EOF, 1=NUMBER, 2=WORD, 3=STRING, 4=OPERATOR, 5=NEWLINE\n\n      (define (emit (typ i32) (start i32) (end i32) -> i32)\n        (let ((p (global.get 0)))\n          (i32.store p typ)\n          (i32.store (+ p 4) start)\n          (i32.store (+ p 8) end)\n          (global.set 0 (+ p 12))\n          (global.set 1 (+ (global.get 1) 1))\n          0))\n\n      ;; --- Main tokenizer ---\n      ;; Input: bytes at offset 0, length passed as argument.\n      ;; Returns: number of tokens emitted.\n\n      (define (tokenize (input-len i32) -> i32)\n        ;; Reset output state\n        (global.set 0 4096)\n        (global.set 1 0)\n        (let ((pos 0))\n          (while (< pos input-len)\n            (let ((ch (i32.load8_u pos)))\n              (if (= ch 10)\n                ;; Newline token\n                (begin (emit 5 pos (+ pos 1))\n                       (set! pos (+ pos 1)))\n                (if (is-space ch)\n                  ;; Skip whitespace\n                  (set! pos (+ pos 1))\n                  (if (is-digit ch)\n                    ;; Number: scan digits\n                    (let ((start pos))\n                      (while (and (< pos input-len)\n                                  (is-digit (i32.load8_u pos)))\n                        (set! pos (+ pos 1)))\n                      (emit 1 start pos))\n                    (if (is-alpha ch)\n                      ;; Word: scan alphanumeric\n                      (let ((start pos))\n                        (while (and (< pos input-len)\n                                    (is-alnum (i32.load8_u pos)))\n                          (set! pos (+ pos 1)))\n                        (emit 2 start pos))\n                      (if (= ch 34)\n                        ;; Double-quoted string: scan to closing quote\n                        (let ((start pos))\n                          (set! pos (+ pos 1))\n                          (while (and (< pos input-len)\n                                      (!= (i32.load8_u pos) 34))\n                            (set! pos (+ pos 1)))\n                          (when (< pos input-len)\n                            (set! pos (+ pos 1)))  ;; skip closing \"\n                          (emit 3 start pos))\n                        ;; Operator: single character\n                        (begin\n                          (emit 4 pos (+ pos 1))\n                          (set! pos (+ pos 1))))))))))\n          ;; Emit EOF\n          (emit 0 pos pos)\n          (global.get 1))))))\n```\n\n### Running the tokenizer\n\n```scheme\n(define mod-h (wasm-sandbox-load tokenizer-bytecode))\n(define inst (wasm-sandbox-instantiate mod-h 'fuel: 5000000))\n\n;; Write input\n(define input (string->utf8 \"count = 42 + x\\n\"))\n(wasm-sandbox-memory-write inst 0 input)\n\n;; Tokenize\n(define n-tokens (wasm-sandbox-call inst \"tokenize\" (bytevector-length input)))\n;; => 6 (WORD, OPERATOR, NUMBER, OPERATOR, WORD, NEWLINE) + EOF = 7\n\n;; Read token table (each token = 12 bytes: type, start, end)\n(define token-data (wasm-sandbox-memory-read inst #x1000 (* n-tokens 12)))\n\n;; Decode tokens back into Scheme\n(define (decode-tokens bv input-bv count)\n  (let loop ((i 0) (tokens '()))\n    (if (= i count) (reverse tokens)\n      (let* ((base (* i 12))\n             (type (bytevector-u32-ref bv base 'little))\n             (start (bytevector-u32-ref bv (+ base 4) 'little))\n             (end (bytevector-u32-ref bv (+ base 8) 'little))\n             (text (utf8->string\n                     (bytevector-copy input-bv start end))))\n        (loop (+ i 1)\n              (cons (list (vector-ref '#(EOF NUMBER WORD STRING OPERATOR NEWLINE)\n                                      type)\n                          text)\n                    tokens))))))\n\n(decode-tokens token-data input n-tokens)\n;; => ((WORD \"count\") (OPERATOR \"=\") (NUMBER \"42\")\n;;     (OPERATOR \"+\") (WORD \"x\") (NEWLINE \"\\n\") (EOF \"\"))\n\n(wasm-sandbox-free inst)\n(wasm-sandbox-free-module mod-h)\n```\n\n---\n\n## Example 4: Parser with Import Callbacks\n\nInstead of writing tokens to memory, the WASM parser calls an imported host\nfunction for each token. This is cleaner for streaming parsers and avoids\noutput-buffer sizing.\n\n```scheme\n;; The WASM program imports an \"emit\" function from the host\n(define parser-bytecode\n  (compile-program\n    '((define-memory 1)\n\n      ;; Import: host-side token handler\n      ;; Args: token_type, start_offset, end_offset\n      (define-import \"env\" \"emit_token\" (i32 i32 i32) ())\n\n      (define (is-space (c i32) -> i32)\n        (or (= c 32) (= c 9)))\n\n      (define (is-digit (c i32) -> i32)\n        (and (>= c 48) (<= c 57)))\n\n      ;; Tokenize: scan digits and words, call emit_token for each\n      (define (parse (len i32) -> i32)\n        (let ((pos 0) (count 0))\n          (while (< pos len)\n            (let ((ch (i32.load8_u pos)))\n              (if (is-space ch)\n                (set! pos (+ pos 1))\n                (if (is-digit ch)\n                  (let ((start pos))\n                    (while (and (< pos len)\n                                (is-digit (i32.load8_u pos)))\n                      (set! pos (+ pos 1)))\n                    (emit_token 1 start pos)\n                    (set! count (+ count 1)))\n                  (let ((start pos))\n                    (set! pos (+ pos 1))\n                    (emit_token 2 start pos)\n                    (set! count (+ count 1)))))))\n          count)))))\n```\n\nTo run this with the **interpreter** (which supports host import functions):\n\n```scheme\n(define rt (make-wasm-runtime))\n\n;; Collected tokens\n(define tokens '())\n\n;; Register import \u2014 the host function called by WASM\n(wasm-runtime-load rt parser-bytecode\n  `((\"env\" . ((\"emit_token\" . ,(lambda (type start end)\n                                  (set! tokens\n                                    (cons (list type start end) tokens))\n                                  0))))))\n\n;; Write input and run\n(define input (string->utf8 \"42 + 7\"))\n(wasm-runtime-memory-set! rt 0 input)\n(wasm-runtime-call rt \"parse\" (bytevector-length input))\n\n(reverse tokens)\n;; => ((1 0 2) (2 3 4) (1 5 6))\n;; Token type 1 = number at \"42\", type 2 = operator at \"+\", type 1 = number at \"7\"\n```\n\n**Note**: Import callbacks currently work with the Scheme interpreter runtime.\nThe Rust wasmi sandbox does not yet expose host function registration (the WASM\nmodule must be self-contained). This is a planned enhancement.\n\n---\n\n## Example 5: State Machine Protocol Parser\n\nParse a simple key=value protocol (like HTTP headers or .env files) entirely\nin WASM.\n\n```scheme\n(define kv-parser-bytecode\n  (compile-program\n    '((define-memory 1)\n\n      ;; Memory layout:\n      ;;   0x0000 .. 0x1FFF  Input (8 KiB)\n      ;;   0x2000 .. 0x3FFF  Output: pairs as (key_start, key_end, val_start, val_end) \u00d7 N\n      (define-global out-ptr i32 #t 8192)\n      (define-global pair-count i32 #t 0)\n\n      (define (emit-pair (ks i32) (ke i32) (vs i32) (ve i32) -> i32)\n        (let ((p (global.get 0)))\n          (i32.store p ks)\n          (i32.store (+ p 4) ke)\n          (i32.store (+ p 8) vs)\n          (i32.store (+ p 12) ve)\n          (global.set 0 (+ p 16))\n          (global.set 1 (+ (global.get 1) 1))\n          0))\n\n      ;; States: 0=start-of-line, 1=in-key, 2=after-eq, 3=in-value\n      (define (parse-kv (len i32) -> i32)\n        (global.set 0 8192)\n        (global.set 1 0)\n        (let ((pos 0) (state 0) (mark 0) (key-start 0) (key-end 0))\n          (while (< pos len)\n            (let ((ch (i32.load8_u pos)))\n              (if (= state 0)\n                ;; Start of line: skip whitespace, begin key\n                (if (or (= ch 10) (= ch 13) (= ch 32))\n                  (set! pos (+ pos 1))\n                  (if (= ch 35)\n                    ;; Comment: skip to end of line\n                    (begin\n                      (while (and (< pos len) (!= (i32.load8_u pos) 10))\n                        (set! pos (+ pos 1)))\n                      (set! pos (+ pos 1)))\n                    (begin\n                      (set! key-start pos)\n                      (set! state 1)\n                      (set! pos (+ pos 1)))))\n                (if (= state 1)\n                  ;; In key: scan to '='\n                  (if (= ch 61)\n                    (begin\n                      (set! key-end pos)\n                      (set! state 2)\n                      (set! pos (+ pos 1)))\n                    (set! pos (+ pos 1)))\n                  (if (= state 2)\n                    ;; After '=': start value\n                    (begin\n                      (set! mark pos)\n                      (set! state 3)\n                      (set! pos (+ pos 1)))\n                    ;; In value: scan to newline\n                    (if (= ch 10)\n                      (begin\n                        (emit-pair key-start key-end mark pos)\n                        (set! state 0)\n                        (set! pos (+ pos 1)))\n                      (set! pos (+ pos 1))))))))\n          ;; Handle final value without trailing newline\n          (when (= state 3)\n            (emit-pair key-start key-end mark pos))\n          (global.get 1))))))\n```\n\nUsage:\n\n```scheme\n(define mod-h (wasm-sandbox-load kv-parser-bytecode))\n(define inst (wasm-sandbox-instantiate mod-h))\n\n(define input (string->utf8\n  \"HOST=localhost\\nPORT=8080\\n# comment\\nDEBUG=true\\n\"))\n(wasm-sandbox-memory-write inst 0 input)\n\n(define n-pairs (wasm-sandbox-call inst \"parse-kv\" (bytevector-length input)))\n;; => 3\n\n;; Read key-value pairs (16 bytes each)\n(define pair-data (wasm-sandbox-memory-read inst #x2000 (* n-pairs 16)))\n\n(define (decode-kv-pairs bv input-bv count)\n  (let loop ((i 0) (pairs '()))\n    (if (= i count) (reverse pairs)\n      (let* ((base (* i 16))\n             (ks (bytevector-u32-ref bv base 'little))\n             (ke (bytevector-u32-ref bv (+ base 4) 'little))\n             (vs (bytevector-u32-ref bv (+ base 8) 'little))\n             (ve (bytevector-u32-ref bv (+ base 12) 'little)))\n        (loop (+ i 1)\n              (cons (cons (utf8->string (bytevector-copy input-bv ks ke))\n                          (utf8->string (bytevector-copy input-bv vs ve)))\n                    pairs))))))\n\n(decode-kv-pairs pair-data input n-pairs)\n;; => ((\"HOST\" . \"localhost\") (\"PORT\" . \"8080\") (\"DEBUG\" . \"true\"))\n\n(wasm-sandbox-free inst)\n(wasm-sandbox-free-module mod-h)\n```\n\n---\n\n## Compiling to .wasm Files\n\nSave compiled bytecode to disk for later loading:\n\n```scheme\n;; Build step: compile and save\n(define bv (compile-program '((define (add (a i32) (b i32) -> i32) (+ a b)))))\n(call-with-output-file \"add.wasm\"\n  (lambda (p) (put-bytevector p bv))\n  '(binary))\n\n;; Runtime: load from file\n(define bv2 (call-with-input-file \"add.wasm\"\n              (lambda (p) (get-bytevector-all p))\n              '(binary)))\n(define mod-h (wasm-sandbox-load bv2))\n```\n\nThis separates compilation (build time) from execution (runtime). The `.wasm`\nfile is a standard WebAssembly binary \u2014 you can inspect it with `wasm-objdump`\nor `wasm2wat` from the WABT toolkit.\n\n---\n\n## String I/O Conventions\n\nWASM has no string type. Two conventions for passing strings between host and\nWASM:\n\n### Convention A: Memory Buffer (recommended)\n\nHost writes UTF-8 bytes to WASM memory, passes offset and length as arguments.\nWASM writes results to a separate memory region. Host reads results back.\n\n```\nHost                          WASM\n |                              |\n |-- write UTF-8 to [0..N) --->|\n |-- call parse(N) ----------->|\n |                              |-- scan bytes, write tokens to [0x1000..]\n |<-- return token_count -------|\n |-- read tokens from [0x1000] -|\n```\n\nPros: Simple, predictable, no imports needed.\nCons: Output buffer must be pre-sized; host must decode the output format.\n\n### Convention B: Import Callbacks (streaming)\n\nWASM calls an imported host function for each output item. The host function\ncan read the relevant bytes from WASM memory using the offsets provided.\n\n```\nHost                          WASM\n |                              |\n |-- write UTF-8 to [0..N) --->|\n |-- call parse(N) ----------->|\n |                              |-- scan bytes...\n |<-- emit_token(type,s,e) ----|  (host reads bytes [s..e) from WASM memory)\n |<-- emit_token(type,s,e) ----|\n |<-- return token_count -------|\n```\n\nPros: No output buffer sizing, streaming-friendly, host builds native objects.\nCons: Requires import function registration (interpreter mode only, currently).\n\n---\n\n## What Works Well in WASM\n\n| Category | Examples |\n|----------|----------|\n| **Byte-level lexers** | Tokenizers, CSV parsers, line splitters |\n| **State machines** | Protocol parsers, regex DFAs, format validators |\n| **Numeric algorithms** | Crypto primitives, hashing, checksums |\n| **Array processing** | Sorting, searching, filtering on i32/f64 arrays |\n| **Fixed-format parsers** | Binary protocols, TLV, DNS packets |\n| **Recursive descent** | Expression parsers, JSON number/bool parsing |\n| **Pattern matchers** | Glob matching, simple regex engines |\n\n---\n\n## Current Limitations\n\n### No heap allocation\nWASM linear memory is flat. There's no `malloc`/`free`. For tree-building\nparsers, you'd need to implement a bump allocator in WASM:\n\n```scheme\n;; Simple bump allocator pattern\n(define-global heap-ptr i32 #t 16384)  ;; start allocating at 16 KiB\n\n(define (alloc (size i32) -> i32)\n  (let ((ptr (global.get heap-ptr)))\n    (global.set heap-ptr (+ ptr size))\n    ptr))\n```\n\n### No closures or higher-order functions\nFunctions can't capture variables. All data passes through arguments, globals,\nor memory.\n\n### No string operations\nNo `string-append`, `substring`, etc. Strings are raw bytes in memory. The\nhost must handle any string construction.\n\n### No exceptions\nWASM has no try/catch. Use return codes (0 = success, negative = error type).\n\n### Numeric types only\nFunction parameters and return values must be i32, i64, f32, or f64.\nCompound results go through memory.\n\n### No garbage collection\nAllocated memory stays allocated. Use a bump allocator that resets between\ncalls, or implement a free list.\n\n---\n\n## jsh Integration Roadmap\n\nThe goal: write a parser in Jerboa's WASM dialect, compile it to a `.wasm`\nfile, and invoke it from inside jsh as a shell command or pipeline stage.\n\n### Phase 1: `wasm` builtin command\n\nA jsh builtin that loads and calls WASM modules:\n\n```bash\n# Load a .wasm file and call an exported function\nwasm call parser.wasm tokenize \"hello world 42\"\n\n# Use in a pipeline \u2014 stdin flows into WASM memory\necho \"HOST=localhost\" | wasm pipe kv-parser.wasm parse-kv\n\n# Persistent module (load once, call many times)\nwasm load parser parser.wasm\nwasm invoke parser tokenize \"input text\"\nwasm unload parser\n```\n\n### Phase 2: `(jsh wasm)` module\n\nScheme-level API for jsh scripts and meta-commands:\n\n```scheme\n;; In jsh meta-command mode (,expr)\n,use \"parser.wasm\"\n,(wasm-call \"parser.wasm\" 'tokenize \"count = 42\")\n```\n\n### Phase 3: Pipeline integration\n\nWASM modules as pipeline stages with stdin/stdout:\n\n```bash\n# WASM tokenizer as a filter\ncat source.txt | wasm pipe tokenizer.wasm tokenize | grep NUMBER\n\n# Chain WASM stages\ncat input.env | wasm pipe kv-parser.wasm parse-kv | wasm pipe validator.wasm check\n```\n\n### Security model\n\nAll jsh WASM execution inherits the full security stack:\n\n- **Fuel metering**: Deterministic termination (no infinite loops)\n- **Memory isolation**: WASM can't read/write jsh process memory\n- **ROP defense**: Rust wasmi keeps WASM execution off the Chez Scheme stack\n- **Import validation**: Capability-gated host function access\n- **Exception boundary**: WASM errors can't leak Chez runtime internals\n- **Module size limits**: Reject oversized `.wasm` files before parsing\n- **Sandbox composition**: jsh's Landlock/seccomp/pledge layers apply on top\n"}
 {"text": "<!-- FILE: docs/completion.md -->\n# Completion: Wiring Fibers into the Clojure Layer\n\n**Goal:** Every Clojure concurrency primitive in Jerboa should be fiber-aware.\nA `go` block should be a fiber, not an OS thread. `core.async` channels\nshould park fibers, not block threads. Atoms, STM, and the component system\nshould all work naturally inside `fiber-httpd` handlers without accidentally\nblocking the scheduler.\n\n**Status:** 2026-04-12 \u2014 All 6 phases implemented.\n\n---\n\n## The Problem\n\nJerboa has two concurrency worlds that don't talk to each other:\n\n| | **Fiber world** | **Clojure world** |\n|---|---|---|\n| Unit of work | Fiber (engine-based green thread) | OS thread (`fork-thread`) |\n| Channel | `fiber-channel` (parks fiber) | `(std csp)` channel (blocks OS thread) |\n| Scheduling | M:N work-stealing on N workers | 1:1 OS threads, unbounded |\n| I/O | epoll-integrated, non-blocking | Blocking syscalls |\n| Atom | N/A (use raw Chez `box`) | `(std misc atom)` \u2014 mutex-locked |\n| STM | N/A | `(std stm)` \u2014 global commit mutex |\n| HTTP | `fiber-httpd` (one fiber/conn) | N/A |\n\n**What breaks today:** If you call `(chan-get! ch)` inside a `fiber-httpd`\nhandler, it blocks the OS worker thread \u2014 starving every fiber on that worker.\nIf you call `(dosync ...)` from two fibers on the same worker, the commit\nmutex can deadlock the scheduler.\n\n---\n\n## Architecture\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  (std clojure)  +  (std csp clj)                \u2502  \u2190 Clojure API surface\n\u2502  get/assoc/atom/swap!/go/>!!/<!!                 \u2502     (unchanged names)\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  (std csp/fiber)  \u2014 fiber-aware CSP channels     \u2502  \u2190 NEW adapter layer\n\u2502  (std atom/fiber)  \u2014 fiber-safe atoms            \u2502\n\u2502  (std stm/fiber)   \u2014 fiber-safe STM             \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  (std fiber)  \u2014 M:N scheduler + work-stealing    \u2502  \u2190 Foundation\n\u2502  (std net io) \u2014 epoll poller                     \u2502\n\u2502  (std net fiber-httpd) \u2014 HTTP server             \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\nThe Clojure API surface stays identical. The backing implementations detect\nwhether they're running inside a fiber context (`current-fiber` returns\nnon-`#f`) and dispatch accordingly \u2014 fiber primitives when in a fiber, OS\nthread primitives when not.\n\n---\n\n## Phase 1: Fiber-Aware core.async\n\n**The single highest-impact change.** `go` becomes `fiber-spawn`, channels\nbecome fiber-channels, and the entire CSP vocabulary works inside\n`fiber-httpd` handlers.\n\n### 1.1 \u2014 `go` spawns a fiber, not a thread\n\n```scheme\n;; Current (std csp clj):\n(define-syntax go\n  (syntax-rules ()\n    [(_ body ...)\n     (let ([ch (make-channel 1)])\n       (fork-thread (lambda () ...))\n       ch)]))\n\n;; New:\n(define-syntax go\n  (syntax-rules ()\n    [(_ body ...)\n     (let ([rt (current-fiber-runtime)])\n       (if rt\n         ;; Inside fiber runtime: spawn a fiber\n         (let ([ch (make-fiber-channel 1)])\n           (fiber-spawn* (lambda ()\n             (guard (exn [#t (fiber-channel-close ch)])\n               (fiber-channel-send ch (begin body ...)))))\n           ch)\n         ;; Outside fiber runtime: fall back to OS thread (existing behavior)\n         (let ([ch (make-channel 1)])\n           (fork-thread (lambda () ...))\n           ch)))]))\n```\n\nWhen running inside `fiber-httpd`, `(go ...)` creates a fiber that costs\n~4KB. When running standalone (tests, scripts), it falls back to OS threads.\nNo user code changes needed.\n\n### 1.2 \u2014 Unified channel type\n\nCreate `(std csp fiber-chan)` that wraps `fiber-channel` with the CSP\nchannel interface:\n\n- `chan-put!` \u2192 `fiber-channel-send` (parks fiber if full)\n- `chan-get!` \u2192 `fiber-channel-recv` (parks fiber if empty)\n- `chan-try-put!` \u2192 `fiber-channel-try-send`\n- `chan-try-get` \u2192 `fiber-channel-try-recv`\n- `chan-close!` \u2192 `fiber-channel-close`\n- Buffer policies: fixed (bounded fiber-channel), sliding, dropping\n\nThe Clojure aliases (`>!`, `<!`, `>!!`, `<!!`) all route through this.\nSince fiber channels already support `fiber-select`, `alts!` maps directly.\n\n### 1.3 \u2014 `alts!` via `fiber-select`\n\n```scheme\n;; Clojure:\n(let [[val ch] (alts! [ch1 ch2 (timeout 1000)])]\n  (println \"got\" val \"from\" ch))\n\n;; Jerboa \u2014 maps to fiber-select:\n(fiber-select\n  [ch1 val => (values val ch1)]\n  [ch2 val => (values val ch2)]\n  [:timeout 1000 => (values nil :timeout)])\n```\n\nProvide an `alts!` function that builds the `fiber-select` call dynamically.\nThis replaces the current `(std csp select)` event-based implementation\nwhen inside a fiber context.\n\n### 1.4 \u2014 `timeout` channels\n\nAlready have `fiber-timeout` which creates a channel that fires after N ms.\nWire it into `(std csp clj)`:\n\n```scheme\n(define (timeout ms)\n  (if (current-fiber-runtime)\n    (fiber-timeout ms)\n    (csp-timeout ms)))  ;; existing OS-thread version\n```\n\n### 1.5 \u2014 `pipeline` / `pipeline-blocking` / `pipeline-async`\n\nThese spawn parallel workers. Inside a fiber runtime, workers should be\nfibers:\n\n- `pipeline`: N fiber workers, all sharing a transducer\n- `pipeline-blocking`: N OS-thread workers via `work-pool-submit!`\n  (for blocking I/O that can't be fibered)\n- `pipeline-async`: Each item gets its own fiber, user provides\n  async-fn that puts result onto a channel\n\n### Deliverable\n\n`(std csp clj)` works identically from user code but runs fibers\ninside `fiber-httpd`. Existing `go`-heavy code gets ~1000x concurrency\nimprovement (OS threads \u2192 fibers) with zero changes.\n\n**Test:** Port the core.async test suite. Run 10K `go` blocks inside\na `fiber-httpd` handler. Verify no OS threads are leaked.\n\n---\n\n## Phase 2: Fiber-Safe Atoms and Watches\n\n### 2.1 \u2014 Non-blocking atom operations\n\nCurrent `(std misc atom)` uses `mutex-acquire` / `mutex-release`. Inside\na fiber, this blocks the OS worker thread. Replace with:\n\n```scheme\n(define (atom-swap! a f . args)\n  (let loop ()\n    (let* ([old (atom-deref a)]\n           [new (apply f old args)])\n      (if (atom-compare-and-set! a old new)\n        (begin (run-watches! a old new) new)\n        (loop)))))\n```\n\nUse CAS (compare-and-set) spin loop instead of mutex. Chez doesn't have\nnative CAS, but we can implement it with a short-held mutex that never\nblocks fibers for more than a few instructions (no I/O or allocation\nunder the lock).\n\nAlternatively, use `(std misc atom)` as-is but document that atom\ncontention is minimal (the mutex is held for nanoseconds, not\nmilliseconds). This may be good enough \u2014 profile before optimizing.\n\n### 2.2 \u2014 Fiber-aware watches\n\nWhen a watch callback does I/O (e.g., logging, sending a notification),\nit must not block the atom's mutex. Current implementation already runs\nwatches outside the lock. Verify this works correctly inside fibers and\nadd a test.\n\n### 2.3 \u2014 Agents backed by fibers\n\nClojure agents use a thread pool for `send` and a separate pool for\n`send-off`. Map these to:\n\n- `send` \u2192 submit to a bounded fiber pool (N fibers, shared channel)\n- `send-off` \u2192 submit to `work-pool-submit!` (for blocking I/O)\n\nAgent error handling (`agent-error`, `restart-agent`, `set-error-handler!`,\n`set-error-mode!`) stays the same.\n\n### Deliverable\n\nAtoms, watches, and agents work safely inside fibers. No mutex\nstarvation, no worker-thread blocking.\n\n---\n\n## Phase 3: Fiber-Safe STM\n\n### 3.1 \u2014 Replace global commit mutex\n\nCurrent `(std stm)` uses a single global mutex for all commits. Two\nfibers in `dosync` on the same worker thread will deadlock (fiber A\nholds the mutex, fiber B on the same worker can't preempt A to release\nit).\n\nOptions:\n\n**Option A \u2014 Optimistic lock-free STM:**\nReplace the commit mutex with a lock-free compare-and-swap on version\nnumbers. Each tvar has a version counter. `dosync` reads versions at\nstart, validates at commit by checking versions haven't changed, and\natomically bumps them. No mutex needed.\n\n```scheme\n(define (tvar-commit! tv expected-version new-val)\n  ;; CAS: if version still matches, update value + version atomically\n  (with-mutex (tvar-lock tv)  ;; per-tvar lock, not global\n    (if (= (tvar-version tv) expected-version)\n      (begin (tvar-version-set! tv (+ expected-version 1))\n             (tvar-value-set! tv new-val)\n             #t)\n      #f)))\n```\n\n**Option B \u2014 Per-tvar locks (MVCC-style):**\nReplace the single global mutex with per-tvar fine-grained locks. Only\nlock the tvars actually written in the transaction. Conflict detection\nvia version stamps.\n\nRecommend Option B \u2014 simpler, proven (this is what Clojure does), and\nper-tvar locks are short-held so fiber starvation is unlikely.\n\n### 3.2 \u2014 `retry` via fiber parking\n\nClojure's `retry` blocks the transaction until a referenced tvar changes.\nMap this to:\n\n```scheme\n(define (stm-retry! read-set)\n  ;; Park the fiber until any tvar in the read-set changes\n  (let ([ch (make-fiber-channel 1)])\n    ;; Register a one-shot watch on each read-set tvar\n    (for-each (lambda (tv)\n      (tvar-add-watch! tv ch)) read-set)\n    ;; Park until any tvar fires\n    (fiber-channel-recv ch)\n    ;; Unregister watches and restart transaction\n    (for-each (lambda (tv)\n      (tvar-remove-watch! tv ch)) read-set)))\n```\n\nThis gives real STM `retry` semantics \u2014 the fiber sleeps until it\nhas a reason to re-run, instead of busy-spinning.\n\n### Deliverable\n\n`(dosync (alter ref f))` works inside fibers without deadlocks. `retry`\nparks the fiber efficiently. Multiple fibers can run concurrent\ntransactions on the same worker thread.\n\n---\n\n## Phase 4: Ring-Style HTTP Middleware\n\n### 4.1 \u2014 Request and response as persistent maps\n\nClojure Ring represents requests and responses as maps. Jerboa's\n`fiber-httpd` uses records. Bridge the gap:\n\n```scheme\n;; Wrap fiber-httpd request record as a Clojure-compatible map\n(define (request->ring req)\n  (hash-map\n    :request-method (request-method req)\n    :uri            (request-path req)\n    :headers        (request-headers req)\n    :body           (request-body req)\n    :server-port    (or (request-header req \"host\") \"\")\n    :scheme         :http))\n\n;; Convert Ring-style response map back to fiber-httpd response\n(define (ring->response m)\n  (respond (get m :status 200)\n           (get m :headers '())\n           (get m :body \"\")))\n```\n\n### 4.2 \u2014 Middleware as function composition\n\n```scheme\n;; Ring middleware: (handler \u2192 handler)\n(define (wrap-logging handler)\n  (lambda (req)\n    (let ([start (current-time 'time-utc)])\n      (let ([resp (handler req)])\n        (log-request req resp start)\n        resp))))\n\n;; Compose middleware (right to left, like Clojure ->):\n(define (ring-app handler . middleware)\n  (fold-left (lambda (h mw) (mw h)) handler middleware))\n\n;; Usage:\n(fiber-httpd-start 8080\n  (ring-app my-handler\n    wrap-logging\n    wrap-json-content-type\n    wrap-cors\n    wrap-exception-handler))\n```\n\n### 4.3 \u2014 Standard middleware library\n\nPort the most-used Ring middleware:\n\n| Middleware | Purpose |\n|---|---|\n| `wrap-json-body` | Parse JSON request body into map |\n| `wrap-json-response` | Serialize response body as JSON |\n| `wrap-params` | Parse query params into `:params` |\n| `wrap-cookies` | Parse/set cookies |\n| `wrap-session` | Session management (in-memory or pluggable) |\n| `wrap-cors` | CORS headers |\n| `wrap-content-type` | Set default content-type |\n| `wrap-not-modified` | 304 responses via ETag/Last-Modified |\n| `wrap-head` | Convert HEAD requests to GET |\n| `wrap-exception` | Catch exceptions, return 500 |\n\n### 4.4 \u2014 Static file serving via sendfile\n\n```scheme\n(define (wrap-static prefix dir)\n  (lambda (handler)\n    (lambda (req)\n      (let ([path (request-path req)])\n        (if (string-prefix? prefix path)\n          (let ([file (path-join dir (substring path (string-length prefix)))])\n            (if (file-exists? file)\n              (respond-file file)       ;; uses fiber-sendfile internally\n              (handler req)))\n          (handler req))))))\n```\n\n### Deliverable\n\nA Clojure Ring-compatible middleware stack that runs on `fiber-httpd`.\nClojure web developers can port their middleware chains directly.\n\n---\n\n## Phase 5: Component System Integration\n\n### 5.1 \u2014 Fiber-aware component lifecycle\n\nThe current `(std component)` starts/stops components in dependency order.\nAdd fiber-runtime as a first-class component:\n\n```scheme\n(define my-system\n  (system-map\n    :fiber-runtime (fiber-runtime-component 4)\n    :http-server   (httpd-component 8080 handler)\n    :db-pool       (db-pool-component \"postgres://...\")\n    :worker        (worker-component)))\n\n(start my-system)\n;; - Creates fiber runtime (4 workers)\n;; - Starts DB pool\n;; - Starts HTTP server on fiber runtime\n;; - Starts background worker fiber\n```\n\n### 5.2 \u2014 Graceful shutdown integration\n\nWire component `stop` into `fiber-httpd-stop!`:\n\n- Stop accepting new connections\n- Drain in-flight requests (configurable timeout)\n- Close connection pools\n- Stop fiber runtime\n\n### 5.3 \u2014 Dependency injection via fiber parameters\n\nComponents that need access to shared resources (DB pool, config) can use\nfiber parameters:\n\n```scheme\n(define *db-pool* (make-fiber-parameter #f))\n(define *config*  (make-fiber-parameter #f))\n\n;; In httpd handler:\n(lambda (req)\n  (let ([pool (*db-pool*)])\n    (with-pooled-connection pool conn\n      (query conn \"SELECT ...\"))))\n```\n\n### Deliverable\n\nA production-ready application scaffold: fiber runtime + HTTP server +\nDB pool + background workers, all managed by the component system with\nclean startup/shutdown.\n\n---\n\n## Phase 6: Bonus \u2014 core.logic and Datalog\n\nLow priority but high wow-factor. miniKanren was born in Scheme.\n\n### 6.1 \u2014 miniKanren (core.logic subset)\n\n```scheme\n(import (std logic))\n\n(run* (q)\n  (fresh (x y)\n    (== q (list x y))\n    (membero x '(1 2 3))\n    (membero y '(a b))\n    (conde\n      [(== x 1) (== y 'a)]\n      [(== x 2) (== y 'b)])))\n;; => ((1 a) (2 b))\n```\n\nPort `microKanren` (50 lines of Scheme!) then build the `core.logic`\nsugar on top. This is a weekend project.\n\n### 6.2 \u2014 Datalog query engine\n\nBuild on miniKanren + persistent maps for an in-memory Datalog:\n\n```scheme\n(import (std datalog))\n\n(def db (-> (empty-db)\n            (assert [:person/name \"Alice\" :person/age 30])\n            (assert [:person/name \"Bob\"   :person/age 25])))\n\n(query db\n  '[:find ?name ?age\n    :where [?e :person/name ?name]\n           [?e :person/age ?age]\n           [(> ?age 27)]])\n;; => #{[\"Alice\" 30]}\n```\n\n### Deliverable\n\nIn-process logic programming and Datalog queries. Makes Jerboa\ninteresting for rule engines and knowledge graphs.\n\n---\n\n## Priority Order\n\n```\nPhase 1  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588  \u2014 core.async on fibers. Unblocks everything else.\nPhase 2  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588    \u2014 Fiber-safe atoms. Required for real handlers.\nPhase 4  \u2588\u2588\u2588\u2588\u2588\u2588      \u2014 Ring middleware. The thing users actually want.\nPhase 3  \u2588\u2588\u2588\u2588\u2588       \u2014 Fiber-safe STM. Needed for complex state.\nPhase 5  \u2588\u2588\u2588\u2588        \u2014 Component system. Production scaffolding.\nPhase 6  \u2588\u2588\u2588         \u2014 core.logic/Datalog. Differentiator.\n```\n\n## Success Criteria\n\n**We win when:**\n\n```scheme\n(import (jerboa prelude))\n(import (std clojure))\n(import (std csp clj))\n(import (std net fiber-httpd))\n\n(def state (atom {}))\n\n(fiber-httpd-start 8080\n  (lambda (req)\n    (go\n      (let [result (<! (async-fetch-data))]\n        (swap! state assoc :last-result result)\n        (respond-json 200 result)))))\n```\n\nOne import. Fibers, channels, atoms, HTTP \u2014 all wired together.\nNo thread starvation. No deadlocks. 100K concurrent connections.\n"}
 {"text": "<!-- FILE: docs/partial-evaluation.md -->\n# Compile-Time Partial Evaluation\n\n## Overview\n\nThe `(std compiler partial-eval)` library provides compile-time partial evaluation capabilities, allowing the compiler to automatically evaluate what it can at compile time and generate specialized versions of functions based on known static arguments.\n\n## Key Features\n\n### 1. Binding-Time Analysis\n\nThe system classifies expressions as either static (compile-time known) or dynamic (runtime dependent):\n\n```scheme\n(import (std compiler partial-eval))\n\n;; Static values - known at compile time\n(static-value? 42)              ; => #t\n(static-value? \"hello\")         ; => #t \n(static-value? #t)              ; => #t\n(static-value? '(quote data))   ; => #t\n\n;; Dynamic values - require runtime evaluation\n(dynamic-value? 'variable)      ; => #t\n(dynamic-value? '(input-port))  ; => #t\n```\n\n### 2. Partial Evaluation Engine\n\nThe core partial evaluator can evaluate expressions when some arguments are static:\n\n```scheme\n;; Create static environment\n(define static-env (make-hashtable symbol-hash eq?))\n(hashtable-set! static-env 'width 800)\n(hashtable-set! static-env 'height 600)\n\n;; Partially evaluate expressions\n(partial-evaluate '(+ width height) static-env)\n;; => 1400\n\n(partial-evaluate '(+ width x) static-env)  \n;; => (+ 800 x)  ; width is folded, x remains dynamic\n\n(partial-evaluate '(* (+ width height) scale) static-env)\n;; => (* 1400 scale)  ; inner addition is folded\n```\n\n### 3. Function Specialization\n\n#### Manual Specialization\n\nUse `define-specialized` to create specialized versions of existing functions:\n\n```scheme\n;; Original function\n(define (rectangle-area width height) (* width height))\n\n;; Specialized for squares (width = height)\n(define-specialized square-area (rectangle-area x) x)\n\n(square-area 10)  ; => 100, equivalent to (rectangle-area 10 10)\n\n;; Specialized for standard aspect ratio\n(define-specialized hd-area (rectangle-area 1920) height)\n\n(hd-area 1080)  ; => 2073600\n```\n\n#### Automatic Specialization with `define/pe`\n\nMark functions for partial evaluation:\n\n```scheme\n(define/pe (power base exponent)\n  (if (= exponent 0)\n    1\n    (* base (power base (- exponent 1)))))\n\n;; When called with static exponent:\n(power x 3)  ; Can be specialized to (* x (* x (* x 1)))\n             ; Which cp0 optimizes to (* x x x)\n```\n\n### 4. Compile-Time Evaluation\n\nForce evaluation of expressions at compile time:\n\n```scheme\n;; Evaluate at compile time and embed result\n(define screen-pixels \n  (compile-time-eval '(* 1920 1080)))  ; => 2073600\n\n;; Complex compile-time computations\n(define lookup-table\n  (compile-time-eval \n    '(let loop ([i 0] [acc '()])\n       (if (= i 256)\n         (reverse acc)\n         (loop (+ i 1) (cons (* i i) acc))))))\n```\n\n### 5. Built-in Optimized Functions\n\nThe library provides several functions optimized for partial evaluation:\n\n#### Power Function\n```scheme\n(power 2 8)    ; => 256 (specialized at compile time if exponent is static)\n(power x 0)    ; => 1 (always optimized)\n(power x 1)    ; => x (identity optimization)\n```\n\n#### Arithmetic Sequences\n```scheme\n(arithmetic-seq 0 2 5)     ; => (0 2 4 6 8)\n(arithmetic-seq 10 -1 3)   ; => (10 9 8)\n```\n\n#### List Operations\n```scheme\n(define (double x) (* x 2))\n(map-const double '(1 2 3))  ; => (2 4 6)\n```\n\n#### Matrix Operations\n```scheme\n(define matrix '((1 2) (3 4)))\n(matrix-scale matrix 3)  ; => ((3 6) (9 12))\n```\n\n### 6. Function Specialization API\n\n#### Programmatic Specialization\n\n```scheme\n;; Generate specialized function code\n(define spec-code \n  (specialize-function 'multiply '(x y) '(* x y) '(10)))\n\n;; spec-code generates:\n;; (define multiply-specialized (lambda (y) (* 10 y)))\n```\n\n### 7. Configuration and Cache Management\n\n```scheme\n;; Enable/disable auto-specialization\n(enable-auto-specialization!)\n(disable-auto-specialization!)\n(auto-specialization-enabled?)  ; => #t/#f\n\n;; Cache management\n(clear-specialization-cache!)\n(dump-specialization-stats)\n```\n\n## Advanced Usage Patterns\n\n### 1. Domain-Specific Optimization\n\n```scheme\n;; Graphics transformations\n(define/pe (transform-2d x y scale-x scale-y translate-x translate-y)\n  (values (+ (* x scale-x) translate-x)\n          (+ (* y scale-y) translate-y)))\n\n;; When scales and translations are known at compile time,\n;; this generates highly optimized code\n\n;; Specialized for common case: uniform scaling with no translation\n(define-specialized transform-uniform (transform-2d x y scale scale 0 0) x y)\n```\n\n### 2. Configuration-Based Specialization\n\n```scheme\n;; Different algorithms based on compile-time config\n(define/pe (sort-algorithm lst algorithm)\n  (case algorithm\n    [(quick) (quicksort lst)]\n    [(merge) (mergesort lst)]  \n    [(heap)  (heapsort lst)]))\n\n;; Specialized for specific algorithm\n(define-specialized quick-sort (sort-algorithm lst 'quick) lst)\n```\n\n### 3. Loop Unrolling\n\n```scheme\n(define/pe (vector-dot-product a b size)\n  (let loop ([i 0] [sum 0])\n    (if (= i size)\n      sum\n      (loop (+ i 1) (+ sum (* (vector-ref a i) (vector-ref b i)))))))\n\n;; For small static sizes, this unrolls into direct operations\n(vector-dot-product va vb 4)  \n;; => Unrolls to: (+ (* (vector-ref va 0) (vector-ref vb 0))\n;;                   (* (vector-ref va 1) (vector-ref vb 1))  \n;;                   (* (vector-ref va 2) (vector-ref vb 2))\n;;                   (* (vector-ref va 3) (vector-ref vb 3)))\n```\n\n## Performance Considerations\n\n### Benefits\n- **Compile-time computation**: Static values computed once at compile time\n- **Reduced branches**: Static conditionals eliminated\n- **Specialized code paths**: Functions optimized for specific argument patterns\n- **Loop unrolling**: Small loops converted to straight-line code\n\n### Costs\n- **Compilation time**: Partial evaluation adds to compile-time overhead\n- **Code size**: Specialization can increase binary size\n- **Analysis overhead**: Binding-time analysis has computational cost\n\n### Best Practices\n\n1. **Use sparingly**: Mark only hot functions with `define/pe`\n2. **Focus on inner loops**: Greatest benefit in tight computational loops  \n3. **Static configuration**: Excellent for compile-time configuration options\n4. **Avoid over-specialization**: Don't specialize functions with many call sites\n\n## Integration with Chez Scheme's cp0\n\nPartial evaluation works synergistically with Chez Scheme's cp0 optimizer:\n\n1. **PE generates simplified code** \u2192 cp0 performs additional optimizations\n2. **Constant folding** \u2192 cp0 propagates constants further\n3. **Dead code elimination** \u2192 cp0 removes unreachable branches\n4. **Inlining** \u2192 cp0 can inline specialized functions more aggressively\n\n## API Reference\n\n### Core Functions\n- `static-value?` - Test if value is compile-time known\n- `dynamic-value?` - Test if value requires runtime evaluation  \n- `partial-evaluate` - Partially evaluate expression with static environment\n- `compile-time-eval` - Force compile-time evaluation\n\n### Specialization\n- `define/pe` - Mark function for partial evaluation\n- `define-specialized` - Create manually specialized function\n- `specialize-function` - Generate specialized function programmatically\n\n### Configuration\n- `enable-auto-specialization!` - Enable automatic specialization\n- `disable-auto-specialization!` - Disable automatic specialization\n- `auto-specialization-enabled?` - Check specialization status\n\n### Cache Management  \n- `clear-specialization-cache!` - Clear cached specializations\n- `dump-specialization-stats` - Print cache statistics\n\n### Built-in Optimized Functions\n- `power` - Exponentiation with compile-time optimization\n- `arithmetic-seq` - Generate arithmetic sequences\n- `map-const` - Map with constant function\n- `matrix-scale` - Scale matrix by constant\n\nThis implementation provides the foundation for high-performance Scheme code through aggressive compile-time optimization while maintaining the expressiveness and simplicity expected in a Lisp environment."}
-{"text": "<!-- FILE: docs/tutorial.md -->\n# Jerboa Tutorial: Build a URL Shortener\n\nThis tutorial walks you from `hello world` to a deployable single-file\nJerboa service. By the end you will have built **Stubby**, a small URL\nshortener with:\n\n- A `defstruct`-modeled core\n- An in-memory cache and a SQLite persistent store\n- An HTTP API on top of `(std net httpd)`\n- A test suite run by `jerboa test`\n- A packaged, redistributable single-file script\n\nEvery code block in this document is a complete runnable or compilable\nJerboa program \u2014 the project's CI verifies that.\n\n---\n\n## 0. Prerequisites\n\n- Chez Scheme \u2265 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---\n\n## 1. Hello, world\n\nCreate `stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Stubby is starting up.\")\n```\n\nRun it:\n\n```bash\njerboa run stubby.ss\n```\n\nOne import, one line of code. `(jerboa prelude)` is the entire\nlanguage \u2014 no separate core/text/number modules to juggle. Everything\nyou'll use below (structs, hash tables, regex, JSON, format, match)\nlives in the prelude.\n\n---\n\n## 2. Model the domain with `defstruct`\n\nA short link maps a **slug** (`abc123`) to a **target URL** and tracks\nhow many times it has been resolved.\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target)\n  (make-short-link slug target 0))\n\n(def (bump-hits! link)\n  (short-link-hits-set! link (+ (short-link-hits link) 1)))\n\n(let ([ln (new-link \"abc\" \"https://example.com\")])\n  (bump-hits! ln)\n  (bump-hits! ln)\n  (printf \"slug=~a target=~a hits=~a~n\"\n          (short-link-slug ln)\n          (short-link-target ln)\n          (short-link-hits ln)))\n```\n\n`defstruct` autogenerates `make-short-link`, `short-link?`, one\naccessor per field (`short-link-slug`), and mutators with the\n`-set!` suffix (`short-link-hits-set!`). No more boilerplate.\n\n---\n\n## 3. Generate slugs\n\nReal shorteners let users pass a custom slug, but fall back to a\nrandom one. We'll use the prelude's `random` and `string-append`:\n\n```scheme\n(import (jerboa prelude))\n\n(def *alphabet*\n  (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(displayln (random-slug))\n(displayln (random-slug 8))\n```\n\nNotes:\n- The ambiguous characters `0 o l 1` are omitted so slugs stay\n  copy-paste-safe over voice/print.\n- `(random N)` returns an integer in `[0, N)`.\n- `(iota n)` is `0..n-1` \u2014 the Jerboa prelude shadows Chez's\n  stricter version to give you the SRFI-1 interface.\n\n---\n\n## 4. An in-memory store\n\nWrap the map in a tiny module-like closure so the rest of the program\nnever touches the hash table directly:\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target) (make-short-link slug target 0))\n\n(def (make-store)\n  (let ([ht (make-hash-table)])\n    (lambda (op . args)\n      (case op\n        [(put!)   (hash-put! ht (car args) (cadr args))]\n        [(get)    (hash-get ht (car args))]\n        [(has?)   (hash-key? ht (car args))]\n        [(all)    (hash-values ht)]\n        [(remove!) (hash-remove! ht (car args))]\n        [else (error 'store \"unknown op\" op)]))))\n\n(def store (make-store))\n(store 'put! \"abc\" (new-link \"abc\" \"https://example.com\"))\n(store 'put! \"xyz\" (new-link \"xyz\" \"https://jerboa.dev\"))\n\n(printf \"abc \u2192 ~a~n\" (short-link-target (store 'get \"abc\")))\n(printf \"total: ~a links~n\" (length (store 'all)))\n```\n\nThis is a deliberate choice to demonstrate closure-based\nencapsulation; the next section swaps it for SQLite without changing\nthe callers.\n\n---\n\n## 5. Persist to SQLite\n\nAdd `(std db sqlite)` and define a store with the same shape. We keep\na write-through cache so reads stay in-memory but survive restarts:\n\n```scheme\n(import (jerboa prelude)\n        (std db sqlite))\n\n(defstruct short-link (slug target hits))\n\n(def (sqlite-store path)\n  (let ([db (sqlite-open path)]\n        [cache (make-hash-table)])\n    (sqlite-exec db\n      \"CREATE TABLE IF NOT EXISTS links (\n         slug   TEXT PRIMARY KEY,\n         target TEXT NOT NULL,\n         hits   INTEGER NOT NULL DEFAULT 0)\")\n    ;; Warm the cache from disk.\n    (for-each\n      (lambda (row)\n        (hash-put! cache (vector-ref row 0)\n          (make-short-link (vector-ref row 0)\n                           (vector-ref row 1)\n                           (vector-ref row 2))))\n      (sqlite-query db \"SELECT slug, target, hits FROM links\"))\n    (lambda (op . args)\n      (case op\n        [(put!)\n         (let ([ln (cadr args)])\n           (sqlite-eval db\n             \"INSERT OR REPLACE INTO links (slug, target, hits) VALUES (?, ?, ?)\"\n             (short-link-slug ln)\n             (short-link-target ln)\n             (short-link-hits ln))\n           (hash-put! cache (car args) ln))]\n        [(get)    (hash-get cache (car args))]\n        [(has?)   (hash-key? cache (car args))]\n        [(all)    (hash-values cache)]\n        [(bump!)\n         (let ([ln (hash-get cache (car args))])\n           (when ln\n             (short-link-hits-set! ln (+ (short-link-hits ln) 1))\n             (sqlite-eval db\n               \"UPDATE links SET hits = hits + 1 WHERE slug = ?\"\n               (car args))))]\n        [(close!) (sqlite-close db)]\n        [else (error 'store \"unknown op\" op)]))))\n\n;; Smoke test:\n(def db-path \"stubby-demo.db\")\n(when (file-exists? db-path) (delete-file db-path))\n(def st (sqlite-store db-path))\n(st 'put! \"abc\" (make-short-link \"abc\" \"https://example.com\" 0))\n(st 'bump! \"abc\")\n(printf \"abc hits: ~a~n\" (short-link-hits (st 'get \"abc\")))\n(st 'close!)\n(delete-file db-path)\n```\n\nKey patterns:\n- `sqlite-exec` for DDL, `sqlite-eval` for parametrized writes,\n  `sqlite-query` for `SELECT`.\n- Parameters bind via `?` with positional args \u2014 never string-append\n  SQL (injection risk, and `jerboa_security_scan` will flag it).\n- `(case op ...)` dispatches symbol ops cheaply; no method-table\n  overhead is needed at this scale.\n\n---\n\n## 6. Serve it over HTTP\n\nJerboa's `(std net httpd)` mirrors the simple-handler shape you'd\nrecognize from Ring (Clojure) or Rack (Ruby): a handler takes a\nrequest alist and returns a response alist.\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std db sqlite))\n\n(defstruct short-link (slug target hits))\n\n;; --- tiny helpers ---------------------------------------------------\n(def (json-response body (status 200))\n  `((status . ,status)\n    (headers . ((\"Content-Type\" . \"application/json\")))\n    (body . ,(json-object->string body))))\n\n(def (redirect-to url)\n  `((status . 302)\n    (headers . ((\"Location\" . ,url)))\n    (body . \"\")))\n\n(def (not-found msg)\n  (json-response\n    (list->hash-table `((\"error\" . ,msg)))\n    404))\n\n;; --- store (re-uses the sqlite-store closure from \u00a75) --------------\n;; For brevity this example uses an in-memory map; swap in\n;; (sqlite-store \"stubby.db\") for persistence.\n(def store\n  (let ([ht (make-hash-table)])\n    (lambda (op . args)\n      (case op\n        [(put!)  (hash-put! ht (car args) (cadr args))]\n        [(get)   (hash-get ht (car args))]\n        [(has?)  (hash-key? ht (car args))]\n        [(bump!)\n         (let ([ln (hash-get ht (car args))])\n           (when ln\n             (short-link-hits-set! ln (+ (short-link-hits ln) 1))))]))))\n\n;; --- handlers -------------------------------------------------------\n(def (handle-create req)\n  (let* ([query (or (request-query req) \"\")]\n         [target (query-param query \"url\")])\n    (if target\n      (let ([slug (random-slug)])\n        (store 'put! slug (make-short-link slug target 0))\n        (json-response\n          (list->hash-table\n            `((\"slug\" . ,slug)\n              (\"target\" . ,target)))\n          201))\n      (not-found \"missing 'url' query parameter\"))))\n\n(def (handle-follow req)\n  (let ([slug (route-param req \"slug\")])\n    (let ([ln (store 'get slug)])\n      (if ln\n        (begin\n          (store 'bump! slug)\n          (redirect-to (short-link-target ln)))\n        (not-found (format \"no such slug: ~a\" slug))))))\n\n(def (handle-stats req)\n  (let ([slug (route-param req \"slug\")])\n    (let ([ln (store 'get slug)])\n      (if ln\n        (json-response\n          (list->hash-table\n            `((\"slug\"   . ,(short-link-slug ln))\n              (\"target\" . ,(short-link-target ln))\n              (\"hits\"   . ,(short-link-hits ln)))))\n        (not-found \"no such slug\")))))\n\n;; --- glue -----------------------------------------------------------\n(def *alphabet* (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(def (query-param query name)\n  (let loop ([pairs (string-split query \"&\")])\n    (cond\n      [(null? pairs) #f]\n      [else\n       (let ([kv (string-split (car pairs) \"=\")])\n         (if (and (= (length kv) 2) (string=? (car kv) name))\n           (cadr kv)\n           (loop (cdr pairs))))])))\n\n(def routes\n  (make-router\n    (route \"POST\" \"/api/links\"         handle-create)\n    (route \"GET\"  \"/api/links/:slug\"   handle-stats)\n    (route \"GET\"  \"/:slug\"             handle-follow)))\n\n;; Uncomment to serve:\n;; (start-httpd 8080 routes)\n(displayln \"Stubby routes compiled; uncomment start-httpd to serve.\")\n```\n\nTest it with `curl`:\n\n```bash\n# Create a short link:\ncurl -X POST 'http://localhost:8080/api/links?url=https%3A%2F%2Fexample.com'\n# \u2192 {\"slug\":\"k4mq9v\",\"target\":\"https://example.com\"}\n\n# Follow it:\ncurl -v http://localhost:8080/k4mq9v\n# \u2192 302 Location: https://example.com\n\n# Get stats:\ncurl http://localhost:8080/api/links/k4mq9v\n# \u2192 {\"slug\":\"k4mq9v\",\"target\":\"...\",\"hits\":1}\n```\n\n---\n\n## 7. Tests\n\nPut this in `tests/test-stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target) (make-short-link slug target 0))\n\n(def pass 0)\n(def fail 0)\n\n(def (check name actual expected)\n  (if (equal? actual expected)\n    (begin (set! pass (+ pass 1))\n           (printf \"  PASS  ~a~n\" name))\n    (begin (set! fail (+ fail 1))\n           (printf \"  FAIL  ~a~n\" name)\n           (printf \"        expected: ~s~n\" expected)\n           (printf \"        got:      ~s~n\" actual))))\n\n(check \"accessors\"\n  (let ([ln (new-link \"abc\" \"https://example.com\")])\n    (list (short-link-slug ln) (short-link-target ln) (short-link-hits ln)))\n  '(\"abc\" \"https://example.com\" 0))\n\n(check \"hits mutator\"\n  (let ([ln (new-link \"abc\" \"https://example.com\")])\n    (short-link-hits-set! ln 3)\n    (short-link-hits ln))\n  3)\n\n(check \"predicate\"\n  (short-link? (new-link \"a\" \"b\"))\n  #t)\n\n(printf \"~n~a passed, ~a failed~n\" pass fail)\n(when (> fail 0) (exit 1))\n```\n\nRun the suite:\n\n```bash\njerboa test tests/\n```\n\n`jerboa test` discovers every `tests/test-*.ss`, runs each with the\nlibrary path preconfigured, and reports pass/fail counts.\n\nFor heavier projects, use `(std test)` (matchers, fixtures,\ndeterministic output) \u2014 see [testing-and-infrastructure.md](testing-and-infrastructure.md).\n\n---\n\n## 8. Package as a single file\n\nThe moment you want to hand `stubby.ss` to a friend, they shouldn't\nneed to clone the Jerboa repo to run it. The\n[single-file package format](single-file-packages.md) lets you\ndeclare dependencies in the file header and `jerboa exec` auto-installs\nthem:\n\n```scheme\n#!/usr/bin/env -S jerboa exec\n;;; jerboa-package\n;;; name:    stubby\n;;; version: 0.1.0\n;;; requires:\n;;;\n;;; Stubby \u2014 minimalist URL shortener.\n;;; Uses only prelude + std modules, so requires: is empty.\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std db sqlite))\n\n;; ...rest of the program...\n(displayln \"Stubby ready.\")\n```\n\nMake it executable and run:\n\n```bash\nchmod +x stubby.ss\n./stubby.ss\n```\n\nIf Stubby later depends on a third-party Jerboa package (say, a\nMarkdown renderer), you add it to `requires:`:\n\n```\n;;; requires:\n;;;   github.com/alice/jerboa-markdown\n```\n\nOn the recipient's first run, `jerboa exec` prompts once to install\nthe missing dependency and then runs the script. Set\n`JERBOA_EXEC_YES=1` for non-interactive CI.\n\n---\n\n## 9. What to read next\n\n- [anti-cookbook.md](anti-cookbook.md) \u2014 multi-form gotchas that\n  catch newcomers (hash-ref arg order, list-of? as a factory, etc.)\n- [module-quickstarts.md](module-quickstarts.md) \u2014 runnable snippets\n  for 22+ stdlib modules\n- [reader-syntax.md](reader-syntax.md) \u2014 bracket/keyword/heredoc\n  rules with exact desugaring\n- [packages.md](packages.md) \u2014 how to publish Jerboa packages\n- [safety-guide.md](safety-guide.md) \u2014 `(jerboa prelude strict)` and\n  capability-based restrictions\n\n## 10. The finished layout\n\n```\nmy-stubby/\n\u251c\u2500\u2500 stubby.ss           # the single-file service\n\u251c\u2500\u2500 tests/\n\u2502   \u2514\u2500\u2500 test-stubby.ss\n\u2514\u2500\u2500 README.md\n```\n\nThat is a complete, deployable Jerboa project: one source file for\nthe program, one directory of tests, no build system, no manifest.\nIf Stubby grows a second source file, move to the multi-file\nlayout documented in [packages.md](packages.md) \u2014 but resist until\nyou have to.\n"}
+{"text": "<!-- FILE: docs/tutorial.md -->\n# Jerboa Tutorial: Build a URL Shortener\n\nThis tutorial walks you from `hello world` to a deployable single-file\nJerboa service. By the end you will have built **Stubby**, a small URL\nshortener with:\n\n- A `defstruct`-modeled core\n- An in-memory cache and a SQLite persistent store\n- An HTTP API on top of `(std net httpd)`\n- A test suite run by `jerboa test`\n- A packaged, redistributable single-file script\n\nEvery code block in this document is a complete runnable or compilable\nJerboa program \u2014 the project's CI verifies that.\n\n---\n\n## 0. Prerequisites\n\n- Chez Scheme \u2265 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---\n\n## 1. Hello, world\n\nCreate `stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Stubby is starting up.\")\n```\n\nRun it:\n\n```bash\njerboa run stubby.ss\n```\n\nOne import, one line of code. `(jerboa prelude)` is the entire\nlanguage \u2014 no separate core/text/number modules to juggle. Everything\nyou'll use below (structs, hash tables, regex, JSON, format, match)\nlives in the prelude.\n\n---\n\n## 2. Model the domain with `defstruct`\n\nA short link maps a **slug** (`abc123`) to a **target URL** and tracks\nhow many times it has been resolved.\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target)\n  (make-short-link slug target 0))\n\n(def (bump-hits! link)\n  (short-link-hits-set! link (+ (short-link-hits link) 1)))\n\n(let ([ln (new-link \"abc\" \"https://example.com\")])\n  (bump-hits! ln)\n  (bump-hits! ln)\n  (printf \"slug=~a target=~a hits=~a~n\"\n          (short-link-slug ln)\n          (short-link-target ln)\n          (short-link-hits ln)))\n```\n\n`defstruct` autogenerates `make-short-link`, `short-link?`, one\naccessor per field (`short-link-slug`), and mutators with the\n`-set!` suffix (`short-link-hits-set!`). No more boilerplate.\n\n---\n\n## 3. Generate slugs\n\nReal shorteners let users pass a custom slug, but fall back to a\nrandom one. We'll use the prelude's `random` and `string-append`:\n\n```scheme\n(import (jerboa prelude))\n\n(def *alphabet*\n  (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(displayln (random-slug))\n(displayln (random-slug 8))\n```\n\nNotes:\n- The ambiguous characters `0 o l 1` are omitted so slugs stay\n  copy-paste-safe over voice/print.\n- `(random N)` returns an integer in `[0, N)`.\n- `(iota n)` is `0..n-1` \u2014 the Jerboa prelude shadows Chez's\n  stricter version to give you the SRFI-1 interface.\n\n---\n\n## 4. An in-memory store\n\nWrap the map in a tiny module-like closure so the rest of the program\nnever touches the hash table directly:\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target) (make-short-link slug target 0))\n\n(def (make-store)\n  (let ([ht (make-hash-table)])\n    (lambda (op . args)\n      (case op\n        [(put!)   (hash-put! ht (car args) (cadr args))]\n        [(get)    (hash-get ht (car args))]\n        [(has?)   (hash-key? ht (car args))]\n        [(all)    (hash-values ht)]\n        [(remove!) (hash-remove! ht (car args))]\n        [else (error 'store \"unknown op\" op)]))))\n\n(def store (make-store))\n(store 'put! \"abc\" (new-link \"abc\" \"https://example.com\"))\n(store 'put! \"xyz\" (new-link \"xyz\" \"https://jerboa.dev\"))\n\n(printf \"abc \u2192 ~a~n\" (short-link-target (store 'get \"abc\")))\n(printf \"total: ~a links~n\" (length (store 'all)))\n```\n\nThis is a deliberate choice to demonstrate closure-based\nencapsulation; the next section swaps it for SQLite without changing\nthe callers.\n\n---\n\n## 5. Persist to SQLite\n\nAdd `(std db sqlite)` and define a store with the same shape. We keep\na write-through cache so reads stay in-memory but survive restarts:\n\n```scheme\n(import (jerboa prelude)\n        (std db sqlite))\n\n(defstruct short-link (slug target hits))\n\n(def (sqlite-store path)\n  (let ([db (sqlite-open path)]\n        [cache (make-hash-table)])\n    (sqlite-exec db\n      \"CREATE TABLE IF NOT EXISTS links (\n         slug   TEXT PRIMARY KEY,\n         target TEXT NOT NULL,\n         hits   INTEGER NOT NULL DEFAULT 0)\")\n    ;; Warm the cache from disk.\n    (for-each\n      (lambda (row)\n        (hash-put! cache (vector-ref row 0)\n          (make-short-link (vector-ref row 0)\n                           (vector-ref row 1)\n                           (vector-ref row 2))))\n      (sqlite-query db \"SELECT slug, target, hits FROM links\"))\n    (lambda (op . args)\n      (case op\n        [(put!)\n         (let ([ln (cadr args)])\n           (sqlite-eval db\n             \"INSERT OR REPLACE INTO links (slug, target, hits) VALUES (?, ?, ?)\"\n             (short-link-slug ln)\n             (short-link-target ln)\n             (short-link-hits ln))\n           (hash-put! cache (car args) ln))]\n        [(get)    (hash-get cache (car args))]\n        [(has?)   (hash-key? cache (car args))]\n        [(all)    (hash-values cache)]\n        [(bump!)\n         (let ([ln (hash-get cache (car args))])\n           (when ln\n             (short-link-hits-set! ln (+ (short-link-hits ln) 1))\n             (sqlite-eval db\n               \"UPDATE links SET hits = hits + 1 WHERE slug = ?\"\n               (car args))))]\n        [(close!) (sqlite-close db)]\n        [else (error 'store \"unknown op\" op)]))))\n\n;; Smoke test:\n(def db-path \"stubby-demo.db\")\n(when (file-exists? db-path) (delete-file db-path))\n(def st (sqlite-store db-path))\n(st 'put! \"abc\" (make-short-link \"abc\" \"https://example.com\" 0))\n(st 'bump! \"abc\")\n(printf \"abc hits: ~a~n\" (short-link-hits (st 'get \"abc\")))\n(st 'close!)\n(delete-file db-path)\n```\n\nKey patterns:\n- `sqlite-exec` for DDL, `sqlite-eval` for parametrized writes,\n  `sqlite-query` for `SELECT`.\n- Parameters bind via `?` with positional args \u2014 never string-append\n  SQL (injection risk, and `jerboa_security_scan` will flag it).\n- `(case op ...)` dispatches symbol ops cheaply; no method-table\n  overhead is needed at this scale.\n\n---\n\n## 6. Serve it over HTTP\n\nJerboa's `(std net httpd)` mirrors the simple-handler shape you'd\nrecognize from Ring (Clojure) or Rack (Ruby): a handler takes a\nrequest alist and returns a response alist.\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std db sqlite))\n\n(defstruct short-link (slug target hits))\n\n;; --- tiny helpers ---------------------------------------------------\n(def (json-response body (status 200))\n  `((status . ,status)\n    (headers . ((\"Content-Type\" . \"application/json\")))\n    (body . ,(json-object->string body))))\n\n(def (redirect-to url)\n  `((status . 302)\n    (headers . ((\"Location\" . ,url)))\n    (body . \"\")))\n\n(def (not-found msg)\n  (json-response\n    (list->hash-table `((\"error\" . ,msg)))\n    404))\n\n;; --- store (re-uses the sqlite-store closure from \u00a75) --------------\n;; For brevity this example uses an in-memory map; swap in\n;; (sqlite-store \"stubby.db\") for persistence.\n(def store\n  (let ([ht (make-hash-table)])\n    (lambda (op . args)\n      (case op\n        [(put!)  (hash-put! ht (car args) (cadr args))]\n        [(get)   (hash-get ht (car args))]\n        [(has?)  (hash-key? ht (car args))]\n        [(bump!)\n         (let ([ln (hash-get ht (car args))])\n           (when ln\n             (short-link-hits-set! ln (+ (short-link-hits ln) 1))))]))))\n\n;; --- handlers -------------------------------------------------------\n(def (handle-create req)\n  (let* ([query (or (request-query req) \"\")]\n         [target (query-param query \"url\")])\n    (if target\n      (let ([slug (random-slug)])\n        (store 'put! slug (make-short-link slug target 0))\n        (json-response\n          (list->hash-table\n            `((\"slug\" . ,slug)\n              (\"target\" . ,target)))\n          201))\n      (not-found \"missing 'url' query parameter\"))))\n\n(def (handle-follow req)\n  (let ([slug (route-param req \"slug\")])\n    (let ([ln (store 'get slug)])\n      (if ln\n        (begin\n          (store 'bump! slug)\n          (redirect-to (short-link-target ln)))\n        (not-found (format \"no such slug: ~a\" slug))))))\n\n(def (handle-stats req)\n  (let ([slug (route-param req \"slug\")])\n    (let ([ln (store 'get slug)])\n      (if ln\n        (json-response\n          (list->hash-table\n            `((\"slug\"   . ,(short-link-slug ln))\n              (\"target\" . ,(short-link-target ln))\n              (\"hits\"   . ,(short-link-hits ln)))))\n        (not-found \"no such slug\")))))\n\n;; --- glue -----------------------------------------------------------\n(def *alphabet* (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(def (query-param query name)\n  (let loop ([pairs (string-split query \"&\")])\n    (cond\n      [(null? pairs) #f]\n      [else\n       (let ([kv (string-split (car pairs) \"=\")])\n         (if (and (= (length kv) 2) (string=? (car kv) name))\n           (cadr kv)\n           (loop (cdr pairs))))])))\n\n(def routes\n  (make-router\n    (route \"POST\" \"/api/links\"         handle-create)\n    (route \"GET\"  \"/api/links/:slug\"   handle-stats)\n    (route \"GET\"  \"/:slug\"             handle-follow)))\n\n;; Uncomment to serve:\n;; (start-httpd 8080 routes)\n(displayln \"Stubby routes compiled; uncomment start-httpd to serve.\")\n```\n\nTest it with `curl`:\n\n```bash\n# Create a short link:\ncurl -X POST 'http://localhost:8080/api/links?url=https%3A%2F%2Fexample.com'\n# \u2192 {\"slug\":\"k4mq9v\",\"target\":\"https://example.com\"}\n\n# Follow it:\ncurl -v http://localhost:8080/k4mq9v\n# \u2192 302 Location: https://example.com\n\n# Get stats:\ncurl http://localhost:8080/api/links/k4mq9v\n# \u2192 {\"slug\":\"k4mq9v\",\"target\":\"...\",\"hits\":1}\n```\n\n---\n\n## 7. Tests\n\nPut this in `tests/test-stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target) (make-short-link slug target 0))\n\n(def pass 0)\n(def fail 0)\n\n(def (check name actual expected)\n  (if (equal? actual expected)\n    (begin (set! pass (+ pass 1))\n           (printf \"  PASS  ~a~n\" name))\n    (begin (set! fail (+ fail 1))\n           (printf \"  FAIL  ~a~n\" name)\n           (printf \"        expected: ~s~n\" expected)\n           (printf \"        got:      ~s~n\" actual))))\n\n(check \"accessors\"\n  (let ([ln (new-link \"abc\" \"https://example.com\")])\n    (list (short-link-slug ln) (short-link-target ln) (short-link-hits ln)))\n  '(\"abc\" \"https://example.com\" 0))\n\n(check \"hits mutator\"\n  (let ([ln (new-link \"abc\" \"https://example.com\")])\n    (short-link-hits-set! ln 3)\n    (short-link-hits ln))\n  3)\n\n(check \"predicate\"\n  (short-link? (new-link \"a\" \"b\"))\n  #t)\n\n(printf \"~n~a passed, ~a failed~n\" pass fail)\n(when (> fail 0) (exit 1))\n```\n\nRun the suite:\n\n```bash\njerboa test tests/\n```\n\n`jerboa test` discovers every `tests/test-*.ss`, runs each with the\nlibrary path preconfigured, and reports pass/fail counts.\n\nFor heavier projects, use `(std test)` (matchers, fixtures,\ndeterministic output) \u2014 see [testing-and-infrastructure.md](testing-and-infrastructure.md).\n\n---\n\n## 8. Package as a single file\n\nThe moment you want to hand `stubby.ss` to a friend, they shouldn't\nneed to clone the Jerboa repo to run it. The\n[single-file package format](single-file-packages.md) lets you\ndeclare dependencies in the file header and `jerboa exec` auto-installs\nthem:\n\n```scheme\n#!/usr/bin/env -S jerboa exec\n;;; jerboa-package\n;;; name:    stubby\n;;; version: 0.1.0\n;;; requires:\n;;;\n;;; Stubby \u2014 minimalist URL shortener.\n;;; Uses only prelude + std modules, so requires: is empty.\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std db sqlite))\n\n;; ...rest of the program...\n(displayln \"Stubby ready.\")\n```\n\nMake it executable and run:\n\n```bash\nchmod +x stubby.ss\n./stubby.ss\n```\n\nIf Stubby later depends on a third-party Jerboa package (say, a\nMarkdown renderer), you add it to `requires:`:\n\n```\n;;; requires:\n;;;   github.com/alice/jerboa-markdown\n```\n\nOn the recipient's first run, `jerboa exec` prompts once to install\nthe missing dependency and then runs the script. Set\n`JERBOA_EXEC_YES=1` for non-interactive CI.\n\n---\n\n## 9. What to read next\n\n- [anti-cookbook.md](anti-cookbook.md) \u2014 multi-form gotchas that\n  catch newcomers (hash-ref arg order, list-of? as a factory, etc.)\n- [module-quickstarts.md](module-quickstarts.md) \u2014 runnable snippets\n  for 22+ stdlib modules\n- [reader-syntax.md](reader-syntax.md) \u2014 bracket/keyword/heredoc\n  rules with exact desugaring\n- [packages.md](packages.md) \u2014 how to publish Jerboa packages\n- [safety-guide.md](safety-guide.md) \u2014 `(jerboa prelude strict)` and\n  capability-based restrictions\n\n## 10. The finished layout\n\n```\nmy-stubby/\n\u251c\u2500\u2500 stubby.ss           # the single-file service\n\u251c\u2500\u2500 tests/\n\u2502   \u2514\u2500\u2500 test-stubby.ss\n\u2514\u2500\u2500 README.md\n```\n\nThat is a complete, deployable Jerboa project: one source file for\nthe program, one directory of tests, no build system, no manifest.\nIf Stubby grows a second source file, move to the multi-file\nlayout documented in [packages.md](packages.md) \u2014 but resist until\nyou have to.\n"}
 {"text": "<!-- FILE: docs/index.md -->\n# Jerboa Documentation\n\nUpdated 2026-03-22.\n\n## Project Overview\n\n- [status.md](status.md) \u2014 Current project status, feature inventory, known limitations\n- [whats-new.md](whats-new.md) \u2014 What's new: 33 modules added in latest push\n- [whatsnew.md](whatsnew.md) \u2014 Earlier additions: translator enhancements and initial stdlib\n- [pending.md](pending.md) \u2014 Roadmap: what's left to make Jerboa better\n- [architecture-split.md](architecture-split.md) \u2014 Design: Rust native / chez-* (FFI backends) vs jerboa (application logic)\n- [native-rust.md](native-rust.md) \u2014 Rust native library: architecture, migration status, C ABI design\n- [rocks.md](rocks.md) \u2014 Distribution and packaging\n\n## Security\n\n- [safety-guide.md](safety-guide.md) \u2014 Practical guide: writing secure Jerboa applications\n- [security-reference.md](security-reference.md) \u2014 API reference for all security modules\n- [ai-threat.md](ai-threat.md) \u2014 AI-assisted adversarial threat model\n- [capability.md](capability.md) \u2014 Capability-based security design\n- [import-conflicts.md](import-conflicts.md) \u2014 R6RS import conflict resolution\n\n## Standard Library Reference\n\n- [regex.md](regex.md) \u2014 Unified regex API, composable `rx`/`define-rx` patterns, PEG grammar system, raw string syntax\n- [data-structures.md](data-structures.md) \u2014 Persistent maps, lazy sequences, weak collections, collection protocol, relations, diff, equiv\n- [concurrency-extended.md](concurrency-extended.md) \u2014 Events, custodians, pools, delimited continuations, continuation marks, amb\n- [metaprogramming.md](metaprogramming.md) \u2014 Typeclasses, CK-macros, format compilation, chaperones, advice, binary types\n- [testing-and-infrastructure.md](testing-and-infrastructure.md) \u2014 QuickCheck, assert!, profiling, config, terminal, highlighting, guardian pools, memoization\n- [protocols.md](protocols.md) \u2014 9P2000 filesystem protocol, MessagePack serialization\n- [fiber.md](fiber.md) \u2014 Green threads / fibers with M:N scheduling\n\n## Networking\n\n- `(std net ssh)` \u2014 Full SSH client: connect, exec, shell, SFTP, port forwarding (10 modules, 3,132 lines)\n\n## Language Features\n\n- [typing.md](typing.md) \u2014 Gradual type system: inference, refinements, GADTs, HKTs\n- [pattern-matching.md](pattern-matching.md) \u2014 Pattern matching\n- [effects.md](effects.md) \u2014 Algebraic effects\n- [sequences.md](sequences.md) \u2014 Lazy sequences and iterators\n- [staging.md](staging.md) \u2014 Multi-stage programming and macros\n- [partial-evaluation.md](partial-evaluation.md) \u2014 Partial evaluation\n- [cp0-passes.md](cp0-passes.md) \u2014 Chez cp0 optimization passes\n\n## Concurrency and Distribution\n\n- [concurrency.md](concurrency.md) \u2014 Concurrency safety toolkit\n- [lightweight-concurrency.md](lightweight-concurrency.md) \u2014 Lightweight/green threads\n- [async.md](async.md) \u2014 Async I/O\n- [stm.md](stm.md) \u2014 Software transactional memory\n- [actor-model.md](actor-model.md) \u2014 Actor model\n- [distributed.md](distributed.md) \u2014 Distributed computing\n\n## Build and Deployment\n\n- [build.md](build.md) \u2014 Build system\n- [single-binary.md](single-binary.md) \u2014 Single-binary compilation\n- [static-binary-gotchas.md](static-binary-gotchas.md) \u2014 Static binary pitfalls\n- [compiling-gerbil-projects.md](compiling-gerbil-projects.md) \u2014 Compiling Gerbil projects on Jerboa\n- [packages.md](packages.md) \u2014 Package management\n- [optimization.md](optimization.md) \u2014 Performance optimization\n\n## Rust Native Backend\n\n- [native-rust.md](native-rust.md) \u2014 Rust native backend architecture and implementation\n- [vs-rust.md](vs-rust.md) \u2014 Jerboa vs Rust comparison\n- [ffi.md](ffi.md) \u2014 FFI interface design\n\n## Developer Experience\n\n- [devex.md](devex.md) \u2014 Developer experience features\n- [repl-protocol.md](repl-protocol.md) \u2014 REPL protocol\n\n## Archive\n\nHistorical planning documents are in [archive/](archive/) for reference.\n"}
 {"text": "<!-- FILE: docs/rocks.md -->\n# World-Class REPL & Standard Library Expansion\n\n## 1. SLIME-Inspired REPL (`lib/std/repl.sls`)\n\nThe REPL went from a basic 425-line read-eval-print loop to a **1500-line interactive powerhouse** modeled after Emacs SLIME for Common Lisp.\n\n**Why it rocks:** Common Lisp developers rave about SLIME because it makes the language *feel alive*. Jerboa now has that same energy:\n\n- **Value history** (`*`, `**`, `***`, `$1`, `$2`...) \u2014 Never lose a computed result. Refer back to any previous value by number. This is the #1 thing people miss when moving from CL to other Schemes.\n- **Deep object inspector** \u2014 Drill into any value: hashtables show entries, records show fields, closures show captured names. Essential for understanding complex data at the REPL.\n- **Function tracing** \u2014 Wrap any function to see calls/returns with args. Debug without adding print statements.\n- **Built-in profiling & benchmarking** \u2014 Measure wall time, CPU time, memory allocation, GC pressure. No external tools needed.\n- **Tab completion** \u2014 Complete any symbol in the environment. Makes discovery effortless.\n- **Inline documentation** \u2014 `,doc car` gives you docs instantly. Register your own with `register-doc!`.\n- **Apropos search** \u2014 `,apropos string` finds every symbol containing \"string\". Perfect for exploration.\n- **Data engineering commands** \u2014 `,table` for formatted tables, `,stats` for descriptive statistics, `,freq` for frequency tables, `,json` for JSON output. Turn your REPL into a data workbench.\n- **Balanced paren check** \u2014 Correctly handles strings and comments, so `\")\"` inside a string doesn't confuse it.\n- **Persistent history** \u2014 Saved to `~/.jerboa_history` across sessions.\n\n---\n\n## 2. SWANK-Like TCP Server (`lib/std/repl/server.sls`)\n\nA TCP server that speaks an s-expression protocol, enabling **editor integration**.\n\n**Why it rocks:** This is the bridge between jerboa and editors like Emacs. Any editor that can open a TCP socket can:\n\n- Evaluate code remotely and get structured results\n- Get tab completions\n- Look up documentation\n- Expand macros\n- Navigate the filesystem\n- Query memory usage and thread info\n\nThe protocol uses `(id method args...) \u2192 (id :ok result)` \u2014 dead simple to implement on the editor side. Port discovery via `~/.jerboa-repl-port` means editors auto-connect. Multi-threaded so multiple editor connections work simultaneously.\n\n---\n\n## 3. REPL Middleware (`lib/std/repl/middleware.sls`)\n\nAn extensibility layer for the REPL \u2014 register custom commands, printers, input transformers, and eval hooks.\n\n**Why it rocks:** Every REPL eventually needs customization. Instead of forking the REPL code, middleware lets users:\n\n- Add `,mycommand` commands without touching core REPL code\n- Register custom pretty-printers for their record types\n- Transform input (e.g., `!ls` \u2192 `(system \"ls\")`)\n- Hook into pre/post eval for logging, timing, or side effects\n- Customize the prompt\n\nThis is the pattern that made Express.js and Rack successful \u2014 composable middleware.\n\n---\n\n## 4. Notebook System (`lib/std/repl/notebook.sls`)\n\nJupyter-style literate programming for Scheme. Save REPL sessions as executable `.ss.nb` files with markdown documentation.\n\n**Why it rocks:** Data scientists live in notebooks. This brings that workflow to Scheme:\n\n- Mix code cells with markdown documentation\n- Capture outputs alongside code\n- Export to HTML (shareable reports) or Markdown (documentation)\n- Record live sessions \u2014 start recording, do your work, stop, save\n- Files are valid Scheme \u2014 you can `load` them directly\n\n---\n\n## 5. Shell Execution (`lib/std/os/shell.sls`)\n\nHigh-level shell command execution with multiple output modes.\n\n**Why it rocks:** Every scripting language needs easy shell access. This gives you:\n\n- `(shell \"ls -la\")` \u2014 just get stdout as a string\n- `(shell! \"make build\")` \u2014 raise on failure (fail-fast scripting)\n- `(shell/lines \"ls\")` \u2014 get a list of lines (no manual splitting)\n- `(shell/status cmd)` \u2014 get stdout, stderr, AND exit code separately\n- `(shell-pipe \"ls\" \"grep .ss\" \"wc -l\")` \u2014 Unix pipes as function args\n- `(shell-env cmd alist)` \u2014 run with custom environment\n- `(shell-async cmd)` \u2014 background execution with later collection\n\nReplaces 50 lines of `open-process-ports` boilerplate with one-liners.\n\n---\n\n## 6. Template Engine (`lib/std/text/template.sls`)\n\nMustache-inspired string templates with sections, iteration, and conditionals.\n\n**Why it rocks:** Code generation, email templates, report formatting \u2014 templates are everywhere. This handles:\n\n- `{{name}}` variable substitution\n- `{{#items}}...{{/items}}` iteration over lists\n- `{{#flag}}...{{/flag}}` conditional sections\n- `{{^empty}}fallback{{/empty}}` inverted sections\n- Compile once, render many times (fast)\n- Works with both symbol and string alist keys\n\nNo external dependencies. No regex. Just clean recursive-descent parsing.\n\n---\n\n## 7. Memoization (`lib/std/misc/memo.sls`)\n\nMemoization with TTL expiry, LRU eviction, and cache introspection.\n\n**Why it rocks:** Memoization is the easiest performance win in functional programming, but most implementations are toy-level. This one is production-grade:\n\n- `(memo fn)` \u2014 simple unbounded memoization\n- `(memo/lru 1000 fn)` \u2014 evicts least-recently-used when cache exceeds size\n- `(memo/ttl 60 fn)` \u2014 entries expire after N seconds (perfect for API caching)\n- `(memo/lru+ttl 1000 60 fn)` \u2014 combined: bounded AND time-limited\n- `(memo-stats fn)` \u2014 hit rate, miss count (is your cache actually helping?)\n- `(defmemo (fib n) ...)` \u2014 syntax sugar for the common case\n\n---\n\n## 8. Retry with Backoff (`lib/std/misc/retry.sls`)\n\nExponential backoff, jitter, predicates, and circuit breaker pattern.\n\n**Why it rocks:** Networks fail. APIs return 503. Databases go down. Without retry logic, your program crashes at 3am. This gives you:\n\n- `(retry thunk 5 1.0)` \u2014 simple retry with fixed delay\n- `(retry/backoff thunk policy)` \u2014 exponential backoff with jitter (prevents thundering herd)\n- `(retry/predicate thunk pred)` \u2014 only retry on specific exceptions\n- **Circuit breaker** \u2014 after N failures, stop trying for a cooldown period. Prevents cascading failures in distributed systems.\n\nThe circuit breaker alone would be a separate library in most ecosystems.\n\n---\n\n## 9. Time Utilities (`lib/std/time.sls`)\n\nHigh-level time operations that Chez doesn't provide out of the box.\n\n**Why it rocks:** Chez has `current-time` but nothing user-friendly. Now you get:\n\n- `(current-timestamp)` \u2014 ISO 8601 string, ready for logs and APIs\n- `(elapsed thunk)` \u2014 measure how long something takes in one call\n- `(time-it \"label\" thunk)` \u2014 print wall + CPU time (like Gerbil's `time`)\n- `(duration->string 3661)` \u2014 \"1h 1m\" (human-readable, auto-scales from \u03bcs to days)\n- **Stopwatch** with lap timing \u2014 perfect for benchmarking multi-phase operations\n- **Throttle/debounce** \u2014 rate-limit function calls (UI patterns, API clients)\n- `(with-timeout 5.0 thunk)` \u2014 kill long-running operations\n\n---\n\n## 10. Result Monad (`lib/std/misc/result.sls`)\n\nRailway-oriented programming without exceptions.\n\n**Why it rocks:** Exceptions are great for unexpected errors but terrible for expected ones (validation, parsing, user input). Result types let you:\n\n- Chain operations that might fail: `(result-> (ok input) (result-map parse) (result-bind validate))`\n- Never forget to handle errors (the type forces you)\n- Collect errors from multiple operations: `(results-collect list-of-results)`\n- Convert between exceptions and results: `(try->result thunk)`\n- Pattern match cleanly: `(result-fold r on-ok on-err)`\n\nThis is the pattern that makes Rust, Haskell, and Elixir code so robust.\n\n---\n\n## 11. Glob Pattern Matching (`lib/std/text/glob.sls`)\n\nFile glob patterns: `*`, `**`, `?`, `[a-z]`, `[!abc]`.\n\n**Why it rocks:** Every time you need to filter files, you reinvent glob matching. Now it's a library:\n\n- `(glob-match? \"*.ss\" \"hello.ss\")` \u2014 pure pattern matching (no filesystem)\n- `(glob-filter \"*.ss\" file-list)` \u2014 filter a list\n- `(glob-expand \"src/**/*.ss\")` \u2014 actual filesystem expansion with recursive `**`\n- `(glob->regex-string pattern)` \u2014 convert to regex for interop\n\nHandles all the edge cases: `**` crosses directories, `*` doesn't, character classes with ranges and negation.\n\n---\n\n## 12. Data Validation (`lib/std/misc/validate.sls`)\n\nComposable validators with structured error messages.\n\n**Why it rocks:** Input validation is tedious and error-prone. Combinators make it declarative:\n\n```scheme\n(define check-user\n  (v-record\n    (list (cons 'name (v-and (v-required \"name\") (v-min-length \"name\" 1)))\n          (cons 'email (v-and (v-required \"email\") (v-pattern \"email\" \"@\")))\n          (cons 'age (v-and (v-integer \"age\") (v-range \"age\" 0 150))))))\n\n(check-user '((name . \"Alice\") (email . \"a@b.com\") (age . 30)))\n; => (values #t '())\n```\n\nValidators compose with `v-and`/`v-or`, work on records/alists, validate collections with `v-each`, and return all errors at once (not just the first one).\n\n---\n\n## 13. Double-Ended Queue (`lib/std/misc/deque.sls`)\n\nO(1) amortized push/pop on both ends using the classic two-list technique.\n\n**Why it rocks:** Lists are great but you can only efficiently access one end. Deques give you both ends, which is essential for:\n\n- BFS algorithms\n- Sliding window problems\n- Work-stealing schedulers\n- Undo/redo stacks\n\nPlus: `deque-map`, `deque-filter`, `list->deque`, bounded mode.\n\n---\n\n## 14. Path Utilities (`lib/std/os/path-util.sls`)\n\nHigher-level filesystem operations that Chez doesn't provide.\n\n**Why it rocks:** Chez has `directory-list` and `file-exists?` but no recursive operations. Now:\n\n- `(path-walk dir proc)` \u2014 Python's `os.walk` for Scheme\n- `(path-find dir predicate)` \u2014 find files matching any condition\n- `(path-glob dir \"*.ss\")` \u2014 find by glob pattern\n- `(with-temp-directory proc)` \u2014 scoped temp dirs with automatic cleanup\n- `(ensure-directory \"a/b/c\")` \u2014 `mkdir -p` equivalent\n- `(copy-file src dst)` \u2014 binary-safe file copy\n\n---\n\n## 15. Text Diff (`lib/std/text/diff.sls`)\n\nLCS-based line diff with unified output and edit distance.\n\n**Why it rocks:** Testing, debugging, and version comparison all need diff:\n\n- `(diff-lines old new)` \u2014 structured diff as `(keep/add/remove line)` entries\n- `(diff-unified \"a\" \"b\" old new)` \u2014 standard unified diff format\n- `(edit-distance \"kitten\" \"sitting\")` \u2014 Levenshtein distance for fuzzy matching\n- `(diff-apply old hunks)` \u2014 apply a diff to reconstruct the new version\n- `(diff-summary hunks)` \u2014 count additions, deletions, unchanged\n\n---\n\n## 16. Ring Buffer (`lib/std/misc/ringbuf.sls`)\n\nFixed-size circular buffer with O(1) operations.\n\n**Why it rocks:** Ring buffers are the backbone of:\n\n- Log rotation (keep last N log entries)\n- Audio/signal processing (sliding windows)\n- Network packet buffers\n- Rate calculation (keep last N timestamps)\n\nWhen full, new elements silently overwrite the oldest. No allocation, no GC pressure, constant memory.\n\n---\n\n## 17. C-Style Printf (`lib/std/text/printf.sls`)\n\n`%d`, `%s`, `%f`, `%x`, `%o`, `%b`, `%e` with width, precision, padding, and alignment.\n\n**Why it rocks:** Chez's `format` is powerful but uses `~a`/`~s` syntax that nobody outside Scheme knows. When porting C/Python/Go code, you want familiar format strings:\n\n```scheme\n(sprintf \"%08x\" 255)           ; => \"000000ff\"\n(sprintf \"%-20s|\" \"hello\")     ; => \"hello               |\"\n(sprintf \"%.2f\" 3.14159)       ; => \"3.14\"\n(sprintf \"%+d\" 42)             ; => \"+42\"\n```\n\nAlso: `cprintf` for stdout, `fprintf*` for ports, `format-one` for single values.\n\n---\n\n## 18. Binary Heap (`lib/std/misc/heap.sls`)\n\nMin-heap and max-heap priority queue with O(log n) operations.\n\n**Why it rocks:** Priority queues are essential for:\n\n- Dijkstra's algorithm\n- Task scheduling (run highest-priority job next)\n- Event-driven simulation\n- Top-K problems\n- Merge K sorted lists\n\n```scheme\n(define h (list->heap < '(5 3 1 4 2)))\n(heap->sorted-list h)  ; => (1 2 3 4 5)\n```\n\nAuto-growing backing array, works with any comparator.\n\n---\n\n## 19. LRU Cache (`lib/std/misc/lru-cache.sls`)\n\nO(1) get/put/evict using hash table + doubly-linked list.\n\n**Why it rocks:** The classic interview question, implemented properly:\n\n- `(lru-cache-get cache key)` \u2014 O(1) lookup\n- `(lru-cache-put! cache key value)` \u2014 O(1) insert with automatic eviction\n- Hit/miss stats with hit rate calculation\n- Key/value iteration in MRU-to-LRU order\n- Thread-safe for read-heavy workloads\n\nMore efficient than `memo/lru` when you need a standalone cache without function wrapping.\n\n---\n\n## 20. Event Emitter (`lib/std/misc/event-emitter.sls`)\n\nNode.js-style pub/sub for decoupled architecture.\n\n**Why it rocks:** When module A needs to notify module B without importing it, events are the answer:\n\n- `(on ee 'data handler)` \u2014 persistent listener\n- `(once ee 'ready handler)` \u2014 fire once then auto-remove\n- `(emit ee 'data 42)` \u2014 fire all handlers\n- `(off ee 'data)` \u2014 unsubscribe\n\nError isolation: one handler crashing doesn't prevent others from running. Essential for plugin architectures and reactive programming.\n\n---\n\n## 21. Trie / Prefix Tree (`lib/std/misc/trie.sls`)\n\nEfficient string prefix operations for autocomplete and search.\n\n**Why it rocks:** The REPL completion engine uses linear search. A trie makes prefix lookup O(k) where k is the prefix length, regardless of dictionary size:\n\n- `(trie-prefix-search t \"str\")` \u2014 all words starting with \"str\"\n- `(trie-autocomplete t \"he\" 10)` \u2014 top 10 completions\n- `(trie-search t \"hello\")` \u2014 exact membership test\n- `(trie-starts-with? t \"hel\")` \u2014 any word with this prefix?\n\nPerfect for command-line autocompletion, spell checking, and IP routing tables.\n\n---\n\n## 22. Token Bucket Rate Limiter (`lib/std/misc/rate-limiter.sls`)\n\nIndustry-standard rate limiting algorithm.\n\n**Why it rocks:** When calling external APIs, you need to respect rate limits or get banned:\n\n- Tokens refill at a constant rate\n- Burst capacity for short spikes\n- `try-acquire` for non-blocking check\n- `acquire!` for blocking wait\n- `with-rate-limit` for clean wrapping\n\nUsed by AWS, Google Cloud, and every major API gateway. Now available in Scheme.\n\n---\n\n## 23. Resource Pool (`lib/std/misc/pool.sls`)\n\nThread-safe generic resource pool with acquire/release semantics.\n\n**Why it rocks:** Database connections, HTTP clients, file handles \u2014 any expensive resource benefits from pooling:\n\n- Creates resources on demand up to max-size\n- Reuses idle resources instead of creating new ones\n- Blocks when pool is exhausted (backpressure)\n- `pool-with-resource` guarantees release via `dynamic-wind`\n- `pool-drain!` for graceful shutdown\n\n---\n\n## 24. Finite State Machine (`lib/std/misc/state-machine.sls`)\n\nDeclarative FSM with transitions, guards, actions, and history.\n\n**Why it rocks:** State machines are the right abstraction for protocols, UI flows, and workflow engines:\n\n```scheme\n(define door (make-state-machine 'locked\n  `((locked   (unlock) unlocked ,log-unlock)\n    (unlocked (open)   opened   ,log-open)\n    (opened   (close)  unlocked ,log-close)\n    (unlocked (lock)   locked   ,log-lock))))\n```\n\n- Declarative transition table (data, not code)\n- Actions fire on transitions\n- Guards can prevent transitions conditionally\n- Full transition history for debugging\n- `sm-can-send?` for UI enable/disable logic\n- On-transition callbacks for cross-cutting concerns\n\n---\n\n## Test Coverage\n\nEvery module has comprehensive tests:\n\n| Batch | Tests | Modules |\n|-------|-------|---------|\n| REPL  | 91    | repl, server, middleware, notebook |\n| Batch 3 | 94  | shell, template, memo, retry, time |\n| Batch 4 | 128 | result, glob, validate, deque, path-util |\n| Batch 5 | 105 | diff, ringbuf, printf, heap, lru-cache |\n| Batch 6 | 70  | event-emitter, trie, rate-limiter, pool, state-machine |\n| **Total** | **488+** | **25 new modules** |\n"}
 {"text": "<!-- FILE: docs/module-quickstarts.md -->\n# Jerboa Module Quickstarts\n\n_One working example per common `(std ...)` module. Each snippet is a\ncomplete runnable `.ss` file \u2014 paste it into `/tmp/x.ss` and run with\n`scheme --libdirs lib --script /tmp/x.ss` from the Jerboa repo root.\nExports referenced here are verified against `api-signatures.json`\n(2026-04-22)._\n\nAll examples assume `(import (jerboa prelude))` at the top \u2014 the\nsnippets add the module-specific import on top of that.\n\n---\n\n## I/O & files\n\n### `(std io)` \u2014 whole-file reads/writes\n\n```scheme\n(import (jerboa prelude) (std io))\n\n(write-all \"/tmp/hello.txt\" \"hi\\nworld\\n\")\n(displayln (read-all \"/tmp/hello.txt\"))\n\n(write-sexp-file \"/tmp/data.sexp\" '((a 1) (b 2)))\n(displayln (read-sexp-file \"/tmp/data.sexp\"))   ; \u2192 ((a 1) (b 2))\n```\n\nExports: `read-all`, `write-all`, `read-sexp-file`, `write-sexp-file`,\n`read-sexp-port`, `write-sexp-port`, `with-input`, `with-output`.\n\n---\n\n### Paths & prelude file helpers\n\n`path-join`, `read-file-string`, `read-file-lines`, `write-file-string`\nare in the **prelude** \u2014 no extra import needed:\n\n```scheme\n(import (jerboa prelude))\n\n(define p (path-join \"/tmp\" \"note.txt\"))\n(write-file-string p \"line 1\\nline 2\\n\")\n(for ((line (in-list (read-file-lines p))))\n  (displayln line))\n```\n\n---\n\n## Text formats\n\n### `(std text json)` \u2014 JSON\n\n```scheme\n(import (jerboa prelude) (std text json))\n\n(define h (string->json-object \"{\\\"name\\\":\\\"Alice\\\",\\\"age\\\":30}\"))\n(hash-ref h \"name\")                        ; \u2192 \"Alice\"\n\n(hash-put! h \"tags\" (vector \"admin\" \"dev\"))\n(displayln (json-object->string h))\n```\n\nJSON objects \u2194 hash tables; JSON arrays \u2194 vectors (not lists).\n\n---\n\n### `(std text yaml)` \u2014 YAML\n\n```scheme\n(import (jerboa prelude) (std text yaml))\n\n;; Parsing: read-yaml-from-string (from full module, not prelude)\n;; See jerboa_module_exports '(std text yaml)' for the full 66-export API \u2014\n;; common entry points: read-yaml-from-string, write-yaml-to-string.\n```\n_YAML module has 66 exports covering streaming, anchors, tags. Use\n`jerboa_module_exports '(std text yaml)'` to see the full list before\nwriting YAML-specific code._\n\n---\n\n### `(std text utf8)` \u2014 byte \u2194 string\n\n```scheme\n(import (jerboa prelude) (std text utf8))\n\n(define bv (string->utf8 \"h\u00e9llo\"))         ; \u2192 bytevector\n(define s  (utf8->string bv))              ; \u2192 \"h\u00e9llo\"\n\n(displayln (bytevector-length bv))         ; 6 (\u00e9 is 2 bytes)\n```\n\n---\n\n### `(std csv)` \u2014 CSV\n\n```scheme\n(import (jerboa prelude) (std csv))\n\n(write-csv-file \"/tmp/a.csv\"\n                '((\"name\" \"age\") (\"Alice\" \"30\") (\"Bob\" \"25\")))\n\n(for ((row (in-list (read-csv-file \"/tmp/a.csv\"))))\n  (displayln row))\n\n;; Row-of-alists mode:\n(define rows (csv->alists \"name,age\\nAlice,30\"))\n(hash-ref (car rows) 'name)                ; \u2192 \"Alice\"\n```\n\n---\n\n### `(std regex)` \u2014 regex (the prelude re-exports the essentials)\n\n```scheme\n(import (jerboa prelude))                   ; re, re-match?, etc. in prelude\n\n(re-match? \"\\\\d+\" \"42\")                    ; \u2192 #t  (full-string match)\n(re-search \"\\\\d+\" \"abc 42 xyz\")            ; \u2192 match-object\n(re-find-all \"\\\\w+\" \"hello world\")         ; \u2192 (\"hello\" \"world\")\n(re-replace-all \"\\\\d\" \"abc1def2\" \"X\")      ; \u2192 \"abcXdefX\"\n(re-split \"\\\\s+\" \"a   b  c\")               ; \u2192 (\"a\" \"b\" \"c\")\n\n;; SRE (symbolic) patterns:\n(define p (rx (+ digit)))\n(re-match? p \"123\")                        ; \u2192 #t\n```\n\nNamed captures (SRE only):\n```scheme\n(define m (re-search (rx (: (=> year (= 4 digit))\n                            \"-\"\n                            (=> month (= 2 digit))))\n                     \"2026-04\"))\n(re-match-named m 'year)                   ; \u2192 \"2026\"\n```\n\n---\n\n### `(std rx patterns)` \u2014 30+ pre-built named patterns\n\n```scheme\n(import (jerboa prelude) (std rx patterns))\n\n(re-match? rx:email \"user@example.com\")    ; \u2192 #t\n(re-match? rx:uuid \"550e8400-e29b-41d4-a716-446655440000\")\n(re-match? rx:ip4 \"192.168.1.1\")\n```\n\n---\n\n### `(std peg)` \u2014 PEG grammars\n\n```scheme\n(import (jerboa prelude) (std peg))\n\n(define-grammar calc\n  (expr   (/ (: term \"+\" expr) term))\n  (term   (/ digits))\n  (digits (+ (/ \"0\" \"1\" \"2\" \"3\" \"4\" \"5\" \"6\" \"7\" \"8\" \"9\"))))\n;; Then use peg-parse or the grammar's entry point.\n```\n_See `jerboa_module_exports '(std peg)'` \u2014 6 exports around\n`define-grammar` and `peg-error-*`._\n\n---\n\n## Networking\n\n### `(std net request)` \u2014 HTTP client\n\n```scheme\n(import (jerboa prelude) (std net request))\n\n(define r (http-get \"https://httpbin.org/get\"))\n(request-status r)                         ; \u2192 200\n(request-text r)                           ; \u2192 body as string\n\n(define r2 (http-post \"https://httpbin.org/post\"\n                      headers: '((\"Content-Type\" . \"application/json\"))\n                      data: \"{\\\"x\\\":1}\"))\n(request-close r2)                         ; always close\n```\n\nURL helpers: `parse-url`, `url-encode`, `build-query-string`,\n`url-parts-host`, `url-parts-port`, `url-parts-path`, `url-parts-scheme`.\n\n---\n\n### `(std net httpd)` \u2014 HTTP server\n\n```scheme\n(import (jerboa prelude) (std net httpd))\n\n;; http-req-* / http-res-* are the handler-API surface.\n;; See jerboa_howto \"httpd\" for a complete canonical handler template \u2014\n;; don't guess the handler signature; the cookbook has it verified.\n```\n\n_28 exports. Use `jerboa_httpd_handler_scaffold` to generate a stub._\n\n---\n\n## OS\n\n### `(std os env)` \u2014 environment variables\n\n```scheme\n(import (jerboa prelude) (std os env))\n\n(getenv \"PATH\")                            ; \u2192 string or #f\n(setenv \"MY_VAR\" \"hello\")\n(unsetenv \"OLD_VAR\")\n```\n\n---\n\n### `(std os signal)` \u2014 signal constants + handlers\n\n```scheme\n(import (jerboa prelude) (std os signal))\n\nSIGINT SIGTERM SIGKILL                     ; numeric constants\n;; See 32 exports including SIGABRT..SIGXCPU plus handler-install hooks.\n```\n\n---\n\n## Concurrency\n\n### `(std misc thread)` \u2014 threads / mutex / CV\n\n```scheme\n(import (jerboa prelude) (std misc thread))\n\n(define t (spawn (lambda () (displayln \"hi from thread\"))))\n(thread-join! t)\n\n(define m (make-mutex))\n(mutex-lock! m)\n(mutex-unlock! m)\n\n;; thread-sleep! is a Gambit-compat wrapper here\n(thread-sleep! 0.1)\n```\n\nNote: Chez must be built with threading enabled for these to work at\nruntime. If you see `variable make-mutex is not bound`, your Chez build\nis non-threaded.\n\n---\n\n### `(std misc process)` \u2014 subprocess\n\n```scheme\n(import (jerboa prelude) (std misc process))\n\n(define p (open-input-process \"ls /tmp\"))\n(for ((line (in-lines p)))\n  (displayln line))\n(close-input-port p)\n```\n\nExports: `open-process`, `open-input-process`, `open-output-process`,\n`filter-with-process`, + 11 more.\n\n---\n\n### `(std actor)` \u2014 actor system\n\n```scheme\n(import (jerboa prelude) (std actor))\n\n;; 57 exports. Use jerboa_actor_ensemble_scaffold to generate a\n;; canonical actor + supervisor skeleton rather than hand-writing.\n```\n\n---\n\n### `(std async)` \u2014 async/await\n\n```scheme\n(import (jerboa prelude) (std async))\n\n;; Primary primitives: async-channel-get, async-channel-put, and the\n;; Async abstract base. See full 14-export API via jerboa_module_exports.\n```\n\n---\n\n## Data\n\n### `(std sort)` \u2014 sort (Chez arg order: predicate first)\n\n```scheme\n(import (jerboa prelude) (std sort))\n\n(sort < '(3 1 4 1 5 9 2 6))                ; \u2192 (1 1 2 3 4 5 6 9)\n(stable-sort string<? '(\"b\" \"a\" \"c\"))      ; \u2192 (\"a\" \"b\" \"c\")\n\n;; In-place versions mutate the input list:\n(sort! > (list 3 1 2))                     ; \u2192 (3 2 1)\n```\n\n**Do not** write `(sort '(3 1 2) <)` \u2014 that's SRFI-95 / Gerbil order,\nwrong in Jerboa.\n\n---\n\n### `(std datetime)` \u2014 dates & times\n\n```scheme\n(import (jerboa prelude))                   ; datetime basics are in prelude\n\n(define now (datetime-now))\n(datetime->iso8601 now)                    ; \u2192 \"2026-04-22T\u2026\"\n(datetime-year now)                         ; \u2192 2026\n\n(define dt (make-datetime 2026 4 22 12 0 0))\n(datetime<? dt now)                         ; \u2192 #t/#f\n\n(day-of-week dt)                            ; \u2192 symbol\n(leap-year? 2024)                           ; \u2192 #t\n```\n\nFull 50-export API includes `parse-datetime`, `duration`,\n`datetime-add`, `datetime-diff`, floor/truncate ops.\n\n---\n\n### `(std crypto digest)` \u2014 hashes\n\n```scheme\n(import (jerboa prelude) (std crypto digest))\n\n(define h (sha256 (string->utf8 \"hello\")))    ; \u2192 digest object\n(digest->hex-string h)                        ; \u2192 \"2cf24d\u2026\"\n\n;; Available digests: md5, sha1, sha224, sha256, sha384, sha512\n```\n\n---\n\n### `(std crypto hmac)` \u2014 HMAC\n\n```scheme\n(import (jerboa prelude) (std crypto hmac))\n\n(define sig (hmac-sha256 (string->utf8 \"key\")\n                         (string->utf8 \"msg\")))\n(digest->hex-string sig)\n```\n\n---\n\n### `(std db sqlite)` \u2014 SQLite\n\n```scheme\n(import (jerboa prelude) (std db sqlite))\n\n;; 28 exports including SQLITE_OK/SQLITE_DONE constants plus connection\n;; and statement APIs. Use jerboa_howto \"sqlite\" for a verified\n;; connection-pool pattern.\n```\n\n---\n\n## Formatting & logging\n\n### `(std format)` \u2014 `format`, `printf`, `fprintf`\n\n`format` and `printf` are already in the prelude (shadowed versions).\nFor explicit control or to get `eprintf`:\n\n```scheme\n(import (jerboa prelude) (std format))\n\n(printf \"~a / ~a = ~a~%\" 10 3 (/ 10 3))\n(eprintf \"ERROR: ~a~%\" \"oh no\")             ; writes to stderr\n(define s (format \"x=~a\" 42))               ; \u2192 \"x=42\"\n```\n\n---\n\n### `(std logger)` \u2014 structured logging\n\n```scheme\n(import (jerboa prelude) (std logger))\n\n(debugf \"processing ~a items\" 42)\n(current-log-directory \"/var/log/myapp\")    ; parameter\n```\n_12 exports; primary entry points: `debugf`, `current-logger`,\n`current-log-directory`._\n\n---\n\n## Compatibility shims\n\n### `(std gambit-compat)` \u2014 Gambit names\n\n**218 exports.** Import this to get Gambit-style names (e.g.,\n`thread-sleep!`, `time->seconds`, `open-fd-pair`-style aliases) working.\nOnly use when porting Gambit code \u2014 for new code, use the native Jerboa\nspelling from the prelude.\n\n### `(std srfi srfi-1)` \u2014 SRFI-1 list library\n\n```scheme\n(import (jerboa prelude) (std srfi srfi-1))\n\n(fold + 0 '(1 2 3 4))                       ; \u2192 10\n(take '(a b c d e) 2)                       ; \u2192 (a b)  (also in prelude)\n(delete-duplicates '(1 2 1 3 2))            ; \u2192 (1 2 3)\n```\n\n71 exports. Most commonly-used SRFI-1 procedures (`map`, `filter`,\n`fold`, `take`, `drop`) are already in the Jerboa prelude under their\nstandard names.\n\n---\n\n## When in doubt\n\nEvery module's export list is authoritative via:\n\n```\njerboa_module_exports '(std text yaml)'\n```\n\n`api-signatures.json` in `jerboa-mcp/` is the single source of truth for\n\"does symbol X exist and where?\" questions \u2014 parsed directly from .sls\nsource files on every regeneration.\n"}
@@ -371,7 +371,7 @@
 {"text": "<!-- FILE: docs/vs-rust.md -->\n# Jerboa vs Rust for Secure Crypto Infrastructure\n\nAn honest analysis of where GC is a real limitation, where it isn't, and what features Jerboa should adopt from Rust to close the gap.\n\n---\n\n## Table of Contents\n\n1. [The Question](#the-question)\n2. [What GC Actually Prevents](#what-gc-actually-prevents)\n3. [What's Not Fundamental \u2014 Just Missing Features](#whats-not-fundamental--just-missing-features)\n4. [What Jerboa Should Steal from Rust](#what-jerboa-should-steal-from-rust)\n5. [The Right Language for Each Layer](#the-right-language-for-each-layer)\n6. [The Honest Assessment](#the-honest-assessment)\n\n---\n\n## The Question\n\nIf you were building a highly secure crypto infrastructure \u2014 key management, encrypted transport, policy enforcement, audit \u2014 would you choose Rust or Jerboa?\n\nThe naive answer is Rust, because memory safety. The real answer is more nuanced, because \"crypto infrastructure\" is a stack with different requirements at each layer, and GC-based memory safety is a different tradeoff than ownership-based memory safety.\n\n---\n\n## What GC Actually Prevents\n\nThere are exactly three things a garbage collector makes impossible or very hard. These are fundamental to the GC model, not implementation bugs that Chez could fix.\n\n### 1. Guaranteed Secret Wiping\n\nWhen you call `bytevector-fill!` to zero a cryptographic key, the GC may have already copied that bytevector during heap compaction. The old copy sits in the heap until it's overwritten by a future allocation. You can't find it, you can't zero it, and a core dump or cold boot attack recovers it.\n\nRust's `Zeroize` trait has a similar problem with the optimizer (LLVM can eliminate \"dead\" stores to memory that's about to be freed), but `Pin` + `ManuallyDrop` + volatile writes give you a fighting chance. With GC, you have no chance \u2014 the collector moves objects without telling you, and you cannot enumerate all previous locations of an object.\n\n**Impact**: Any time a cryptographic key, password, token, or other secret exists as a Scheme bytevector, copies may persist in the heap indefinitely.\n\n### 2. Deterministic Timing\n\nGC pauses inject unpredictable latency. In a protocol where you need constant-time behavior \u2014 MAC verification must take the same time regardless of where the mismatch is, TLS record processing must not reveal plaintext length through timing \u2014 a GC pause between two timing measurements corrupts the guarantee.\n\nThe primitive operation itself might be constant-time (via OpenSSL's `CRYPTO_memcmp` called through FFI), but the Scheme wrapper around it isn't. If the GC fires between \"start timer\" and \"end timer,\" the attacker sees variable latency and can extract information.\n\n**Impact**: Any Scheme-level timing measurement is unreliable. Constant-time operations must be pushed entirely into C/FFI, with no Scheme allocation between the timing-sensitive start and end points.\n\n### 3. Memory Layout Control\n\nYou can't place two keys in adjacent cache lines to minimize cache side-channel exposure. You can't ensure a nonce counter is in a specific memory region. You can't use `mlock` reliably because GC moves the object after you've locked its page. You can't use `madvise(MADV_DONTDUMP)` on a Scheme object because it might be relocated before the core dump happens.\n\n**Impact**: Side-channel mitigations that depend on memory placement (cache-line isolation, page-level protections) are impossible for GC-managed objects.\n\n### What GC Does NOT Prevent\n\nThese three limitations affect a narrow part of the security stack: **secret material handling and timing-critical operations**. They do not affect:\n\n- Capability enforcement\n- Access control policy logic\n- Audit logging\n- Schema validation\n- Protocol state machines (correctness, not timing)\n- Key management policy (who can use which key for what)\n- Sandbox enforcement\n- Taint tracking\n- Contract verification\n- Effect tracking\n\nFor all of the above, GC is irrelevant to security. The language features that matter are expressiveness, type safety, and correctness guarantees \u2014 areas where Jerboa has advantages over Rust.\n\n---\n\n## What's Not Fundamental \u2014 Just Missing Features\n\nRust has several features that improve security. Most of them are not inherent to Rust's memory model \u2014 they're language features that could be added to Jerboa.\n\n### Compile-Time Exhaustive Matching\n\n**Rust**: `match` on an `enum` is exhaustive \u2014 forget a variant and the compiler rejects it. This matters for security state machines where a missing case means a missing security check.\n\n**Jerboa today**: `match` has a runtime catch-all. Missing a case silently falls through.\n\n**Jerboa could add**:\n\n```scheme\n;; Hypothetical: exhaustive-match that requires listing all known variants\n(defstruct message-type)\n(defstruct (handshake-init message-type) fields: (client-hello))\n(defstruct (handshake-response message-type) fields: (server-hello))\n(defstruct (data message-type) fields: (payload))\n(defstruct (close message-type) fields: (reason))\n\n(exhaustive-match msg message-type\n  [(handshake-init? msg) ...]\n  [(handshake-response? msg) ...]\n  [(data? msg) ...]\n  ;; compile-time error: missing (close? msg) case\n  )\n```\n\n**Implementation**: A `defstruct`-aware macro that knows the variant list for a parent type and verifies at macro-expansion time that all variants are covered. No runtime cost, no language change \u2014 just a macro.\n\n### Compile-Time Lifetime Tracking\n\n**Rust**: The borrow checker catches use-after-free, double-use, and forgotten-drop at compile time.\n\n**Jerboa today**: Linear and affine types catch these at runtime \u2014 use a value twice and you get an error when the code runs, not when it compiles.\n\n**Jerboa could add**: A static analysis pass that checks linear consumption for annotated code. It wouldn't be as complete as Rust's borrow checker (Chez's macro system isn't a full type checker), but it could catch the obvious cases before the code runs:\n\n- Double use of a linear value\n- Scope exit without consuming a linear value\n- Passing a linear value to a function that doesn't consume it\n\nThink of it as a lint pass, not a type system. Catches 80% of the bugs at 20% of the implementation cost.\n\n### Send/Sync \u2014 Compile-Time Thread Safety\n\n**Rust**: `Send` and `Sync` traits prevent data races at compile time. A non-`Send` type cannot be moved to another thread. Period.\n\n**Jerboa today**: Runtime annotations (`defstruct/thread-safe`, `defstruct/immutable`) and STM. Thread safety is enforced by convention and runtime checks.\n\n**Jerboa could add**: A static analysis pass that verifies mutable structs don't cross thread boundaries without synchronization. Inspect `spawn`, `thread-start!`, `channel-put`, and `send` calls for arguments that are mutable, non-thread-safe structs.\n\n### Sum Types with Associated Data\n\n**Rust**: Enums carry data per variant \u2014 `Result<T, E>` is `Ok(T) | Err(E)`. Pattern matching destructures them.\n\n**Jerboa today**: GADTs provide tagged constructors with fields, but they're runtime-checked. `defstruct` with subtypes is close but doesn't integrate with exhaustive matching.\n\n**Jerboa could add**: Tighter integration between `defstruct` variant hierarchies and `match`, enabling the exhaustive matching described above plus ergonomic destructuring.\n\n### No-Panic / Infallible APIs\n\n**Rust**: Distinguishes `Result<T, E>` (recoverable) from `panic!` (unrecoverable). Libraries document which functions can panic and which return `Result`.\n\n**Jerboa today**: Any function can raise any condition. There's no way to declare or verify that a function is total (always returns a value, never raises).\n\n**Jerboa could add**:\n\n```scheme\n;; define/total: statically verified to never raise\n(define/total (parse-config sexp)\n  ;; compile-time error if body contains: error, raise, assert, or calls\n  ;; to non-total functions without guard/catch\n  (validate-schema config-schema sexp))\n```\n\nThis is valuable for security-critical code paths where you need the guarantee that error handling cannot be bypassed by an unexpected exception. If a capability check is `define/total`, you know it always returns allow/deny \u2014 it never crashes in a way that might default to allow.\n\n### Typestate Enforcement\n\n**Rust**: Session types and typestate patterns via `enum` transitions \u2014 a TCP socket in state `Connected` has different methods than one in state `Listening`.\n\n**Jerboa today**: Phantom types provide runtime state tracking with `phantom-check` and `phantom-transition`.\n\n**Jerboa could add**: Compile-time transition checking. The phantom type registry already declares valid transitions \u2014 a macro could verify at expansion time that code only calls methods valid for the declared state.\n\n---\n\n## What Jerboa Should Steal from Rust\n\nRanked by impact on closing the security gap:\n\n### 1. Foreign Region Allocator \u2014 Eliminates the #1 GC Limitation\n\nA non-GC-managed memory region for secrets. This is the single most important feature for crypto infrastructure.\n\n```scheme\n;; Secure memory region outside GC's reach\n(with-secure-region ([key (secure-alloc 32)]\n                     [nonce (secure-alloc 12)])\n  ;; key and nonce are in mlock'd memory, invisible to GC\n  ;; they are bytevector-like but NOT managed by the collector\n  (secure-random-fill! key)\n  (secure-random-fill! nonce)\n  (let ([ciphertext (aead-encrypt key nonce plaintext)])\n    ;; ciphertext is GC-managed (not secret) \u2014 that's fine\n    ciphertext))\n;; key and nonce are zeroed and munlock'd here \u2014 guaranteed\n;; even on exception \u2014 because dynamic-wind + explicit_bzero\n```\n\n**Implementation**:\n\n| Component | Mechanism |\n|-----------|-----------|\n| Allocation | `mmap(MAP_PRIVATE \\| MAP_ANONYMOUS)` via FFI |\n| Page locking | `mlock()` \u2014 prevents swapping to disk |\n| Core dump protection | `madvise(MADV_DONTDUMP)` \u2014 excluded from core dumps |\n| Guard pages | `mprotect(PROT_NONE)` on pages before and after \u2014 catches buffer overflows |\n| Wiping | `explicit_bzero()` \u2014 guaranteed not optimized away |\n| Scope management | `dynamic-wind` \u2014 wipe runs even on exception or continuation escape |\n| GC isolation | Region is outside the Chez heap \u2014 collector never sees it, never copies it |\n\nThis is the pattern libsodium uses (`sodium_malloc` / `sodium_free` / `sodium_mprotect_noaccess`). Chez can implement it entirely via FFI to `mmap`/`mlock`/`madvise`. The secure region is outside the GC heap, so the collector never sees it, never copies it, never leaves remnants.\n\nThe secure allocator would integrate with Jerboa's existing affine types:\n\n```scheme\n;; Secure region values are automatically affine \u2014 use at most once\n;; Guardian ensures wiping even if the programmer forgets\n(define key (secure-alloc/affine 32 cleanup: secure-wipe!))\n;; ... use key ...\n(affine-drop! key)  ;; explicit wipe \u2014 or auto-wipe on scope exit\n```\n\n**Effort**: 3-5 days. The FFI calls are straightforward. The integration with `dynamic-wind` and affine types requires care but Jerboa already has both primitives.\n\n### 2. Compile-Time Exhaustive Match\n\nCatches missed security states at compile time. A TLS state machine with 8 states and a `match` that only handles 7 is a vulnerability \u2014 and today Jerboa only catches it when the 8th state is reached at runtime.\n\n```scheme\n(define-variants tls-state\n  tls-idle tls-handshake-sent tls-handshake-received\n  tls-established tls-closing tls-closed\n  tls-error tls-resumed)\n\n(exhaustive-match (connection-state conn) tls-state\n  [(tls-idle) ...]\n  [(tls-handshake-sent) ...]\n  [(tls-handshake-received) ...]\n  [(tls-established) ...]\n  [(tls-closing) ...]\n  [(tls-closed) ...]\n  [(tls-error) ...]\n  ;; COMPILE-TIME ERROR: missing tls-resumed case\n  )\n```\n\n**Effort**: 2-3 days. Macro that tracks variant registrations and checks coverage at expansion time.\n\n### 3. Static Linearity Checking\n\nCatches use-after-drop before runtime. Today, using a dropped affine value is a runtime error. A lint pass could catch the obvious cases statically.\n\n```scheme\n(with-linear ([key (derive-key password salt)])\n  (encrypt key plaintext)\n  (encrypt key plaintext)  ;; STATIC WARNING: key already consumed\n  )\n```\n\n**Effort**: 3-5 days. Static analysis over SSA-like form of the code. Won't catch everything (higher-order, conditional consumption), but catches the common mistakes.\n\n### 4. Total Function Annotation\n\nGuarantees critical paths complete without exception.\n\n```scheme\n(define/total (check-permission user resource action)\n  ;; Guaranteed to return 'allow or 'deny \u2014 never raises\n  ;; Compiler verifies: no unguarded calls to non-total functions\n  (if (and (valid-user? user)\n           (capability-permits? user resource action))\n    'allow\n    'deny))\n```\n\n**Effort**: 2-3 days. Macro that analyzes body for `error`/`raise` calls and unguarded calls to non-total functions.\n\n### 5. Typestate at Compile Time\n\nExtends phantom types with compile-time transition verification.\n\n```scheme\n(define-protocol tls-protocol\n  (transitions\n    [idle -> handshake-sent]\n    [handshake-sent -> handshake-received error]\n    [handshake-received -> established error]\n    [established -> closing]\n    [closing -> closed]))\n\n;; Compile-time error: cannot transition from idle to established\n(phantom-transition conn 'idle 'established)\n```\n\n**Effort**: 3-5 days. Extends the existing phantom type registry with compile-time checking.\n\n### Summary Table\n\n| Feature | What It Fixes | Effort | Priority |\n|---------|--------------|--------|----------|\n| Secure region allocator | Secret wiping, memory layout, mlock | 3-5 days | Critical |\n| Exhaustive match | Missed security states | 2-3 days | High |\n| Static linearity | Use-after-drop at compile time | 3-5 days | High |\n| Total functions | Exception-bypass in security checks | 2-3 days | Medium |\n| Typestate checking | Protocol state machine correctness | 3-5 days | Medium |\n| Send/Sync analysis | Thread safety | 3-5 days | Lower |\n\n---\n\n## The Right Language for Each Layer\n\nNo single language is optimal for all layers of a crypto infrastructure. The right answer is a stack.\n\n### Crypto Primitives (AES, SHA, ECDSA, key derivation)\n\n**Best language**: C or assembly.\n\nNot because C is safe \u2014 it's the opposite \u2014 but because crypto primitives need:\n\n- Constant-time guarantees (no branch on secret data, no secret-dependent memory access)\n- Precise control over memory layout (to wipe keys, avoid optimizer-eliminated stores)\n- No GC that copies secrets to unpredictable locations\n- SIMD intrinsics for performance (AES-NI, CLMUL, AVX2)\n\nNo high-level language gives you all of that. In practice, you don't write your own \u2014 you use libsodium or OpenSSL's libcrypto.\n\n### Protocol Layer (TLS, key exchange, certificate validation, message framing)\n\n**Best language**: Rust.\n\nThis layer is where most real-world crypto bugs live. Heartbleed was a bounds check bug in the TLS heartbeat handler, not in any cipher. This layer needs:\n\n- Memory safety (buffer overflows are the #1 vulnerability class in protocol implementations)\n- Strong type system (state machines encoded as types)\n- No GC pauses (matters for timing-sensitive protocols)\n- Good FFI to call the C primitives underneath\n\nRust delivers all of this. `rustls` is proof \u2014 a TLS implementation with zero memory safety CVEs, compared to OpenSSL's hundreds.\n\n### Application Security Layer (key management, access control, audit, policy)\n\n**Best language**: Jerboa (with the features above) or a similarly expressive language.\n\nThis layer needs:\n\n| Requirement | Jerboa Feature | Rust Equivalent |\n|-------------|---------------|----------------|\n| Capability-based access control | First-class capabilities with attenuation | Manual, via ownership patterns |\n| Audit logging with mediated I/O | Effect system with handlers | No native support |\n| Sandboxed evaluation | `restricted-eval` with environment control | Not possible without separate process |\n| Runtime contract verification | `define/contract` with pre/post/proves | Only `debug_assert!` |\n| Key policy enforcement | Taint tracking + capabilities + types | External tools (clippy lints) |\n| Hot code reload | Native (load + mtime tracking) | Requires restart |\n| Formal verification hooks | Proof-carrying contracts via macros | Requires external provers |\n| Key lifetime management | Affine types + secure regions + guardians | RAII + `Zeroize` + `Pin` |\n\nThe stack:\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Policy / Key Management / Audit / Access Control               \u2502\n\u2502  Jerboa                                                         \u2502\n\u2502  (capabilities, effects, contracts, taint tracking, sandbox)    \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  Protocol Layer                                                 \u2502\n\u2502  Rust                                                           \u2502\n\u2502  (memory safety, no GC, strong types, state machines)           \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  Crypto Primitives                                              \u2502\n\u2502  C / assembly (via libsodium or libcrypto)                      \u2502\n\u2502  (constant-time, hardware control, SIMD, timing guarantees)     \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\nEach layer uses the language whose strengths match the threat model at that layer. The primitives need hardware control. The protocol needs memory safety without GC. The policy layer needs expressiveness.\n\n---\n\n## The Honest Assessment\n\n### If Forced to Pick One Language\n\n**Rust**. It's the best single-language compromise: memory safe without GC, good FFI to C primitives, strong enough types for protocol state machines, and while its application layer isn't as expressive as Jerboa's, it's adequate. The ecosystem (ring, rustls, RustCrypto) is mature and audited.\n\n### Where Jerboa Wins Today\n\nFor key management services, policy-driven encryption gateways, or any system where the hard problem is \"who can decrypt what under which conditions\" rather than \"implement AES correctly,\" Jerboa is already more expressive than Rust:\n\n- **Capabilities** can't be forged (after V2 fix), attenuate monotonically, and integrate with the type system\n- **Effects** make I/O explicit and interceptable \u2014 every key operation can be mediated and audited\n- **Contracts** enforce API invariants at every boundary\n- **Taint tracking** prevents secret data from flowing to public outputs\n- **Sandbox** enables safe evaluation of user-provided policy expressions\n- **Hot reload** enables key rotation and policy updates without downtime\n\n### Where Jerboa Loses Today\n\n- **Secret material in memory** \u2014 GC copies keys. Fixed by the secure region allocator.\n- **Timing guarantees** \u2014 GC pauses. Mitigated by doing timing-critical work in FFI.\n- **Compile-time safety** \u2014 exhaustiveness, linearity, thread safety are runtime-only. Fixed by the features above.\n- **Ecosystem maturity** \u2014 Rust has audited, production-deployed crypto libraries. Jerboa has design documents.\n\n### After the Proposed Features\n\nWith the secure region allocator and compile-time exhaustive match, Jerboa's security story becomes:\n\n| Layer | Jerboa Status | vs Rust |\n|-------|--------------|---------|\n| Crypto primitives | Call libsodium/libcrypto via FFI | Equal (both call C) |\n| Secret handling | Secure regions + affine types + guardians | On par with `Zeroize` + `Pin` |\n| Protocol state machines | Exhaustive match + phantom types + typestate | Comparable to Rust enums |\n| Application security | Capabilities + effects + contracts + taint + sandbox | Significantly ahead |\n| Constant-time operations | FFI to C for timing-critical paths | Slight disadvantage (GC pauses around FFI calls) |\n\nGC is the limitation for roughly 5% of the crypto stack \u2014 secret material in memory and timing-critical operations. The secure region allocator covers most of that 5%. The remaining sliver \u2014 true constant-time execution at the Scheme level \u2014 requires C/asm and always will, in any GC'd language.\n\nFor the other 95% of crypto infrastructure, Jerboa is already more expressive than Rust, and the features proposed here would widen that gap.\n"}
 {"text": "<!-- FILE: docs/packages.md -->\n# Package Manager\n\nJerboa includes a semver-based package manager for declaring, resolving,\nand locking dependencies between Jerboa/Chez Scheme libraries.\n\n**Import:** `(jerboa pkg)`\n\n---\n\n## Package Manifest\n\nA package is described by its name, version, and dependencies.\n\n```scheme\n(make-package name version deps description)\n;; name:        symbol\n;; version:     version record (see below)\n;; deps:        list of dep-spec records\n;; description: string\n\n(package? x)\n(package-name pkg)\n(package-version pkg)\n(package-dependencies pkg)\n(package-description pkg)\n```\n\n---\n\n## Versions\n\nVersions follow semantic versioning: `major.minor.patch`.\n\n```scheme\n(make-version major minor patch)\n(version? x)\n(version-major v)\n(version-minor v)\n(version-patch v)\n(version->string v)       ; => \"1.2.3\"\n(string->version \"1.2.3\") ; => #<version 1.2.3>\n\n(version<? v1 v2)         ; #t if v1 < v2\n(version=? v1 v2)         ; #t if v1 = v2\n(version-satisfies? v constraint)\n;; constraint examples:\n;;   \"^1.2.0\"   \u2014 compatible (same major, >= minor.patch)\n;;   \"~1.2.0\"   \u2014 patch compatible (same major.minor, >= patch)\n;;   \">=1.0.0\"  \u2014 minimum version\n;;   \"1.2.3\"    \u2014 exact version\n```\n\n### Constraint semantics\n\n| Constraint | Meaning |\n|------------|---------|\n| `^1.2.3` | `>=1.2.3 <2.0.0` \u2014 compatible with 1.x |\n| `~1.2.3` | `>=1.2.3 <1.3.0` \u2014 patch updates only |\n| `>=1.0.0` | at least 1.0.0 |\n| `1.2.3` | exactly 1.2.3 |\n\n```scheme\n(version-satisfies? (string->version \"1.3.0\") \"^1.2.0\")   ; => #t\n(version-satisfies? (string->version \"2.0.0\") \"^1.2.0\")   ; => #f\n(version-satisfies? (string->version \"1.2.5\") \"~1.2.0\")   ; => #t\n(version-satisfies? (string->version \"1.3.0\") \"~1.2.0\")   ; => #f\n```\n\n---\n\n## Dependency Specs\n\n```scheme\n(make-dep-spec name constraint source)\n;; name:       package name (symbol)\n;; constraint: version constraint string\n;; source:     'registry | \"https://...\" | \"file:../path\"\n\n(dep-spec? x)\n(dep-spec-name ds)\n(dep-spec-constraint ds)\n(dep-spec-source ds)\n```\n\n---\n\n## Package Registry\n\nThe registry maps package names to available versions.\n\n```scheme\n(make-registry)\n(registry? x)\n(registry-add! reg pkg)          ; add a package\n(registry-find reg name version) ; find exact version; returns pkg or #f\n(registry-list reg name)         ; list all versions of a package\n```\n\n```scheme\n(define reg (make-registry))\n\n(registry-add! reg\n  (make-package 'web-server\n    (string->version \"1.0.0\")\n    (list (make-dep-spec 'http-parser \"^2.0.0\" 'registry))\n    \"A simple web server\"))\n\n(registry-add! reg\n  (make-package 'web-server\n    (string->version \"1.1.0\")\n    (list (make-dep-spec 'http-parser \"^2.1.0\" 'registry))\n    \"A simple web server\"))\n\n(registry-list reg 'web-server)\n; => (#<package web-server 1.0.0> #<package web-server 1.1.0>)\n```\n\n---\n\n## Dependency Resolution\n\n```scheme\n(resolve-dependencies registry root-deps)\n;; root-deps: list of dep-specs\n;; Returns: list of package records in topological order (deps before dependents)\n\n(dependency-graph registry deps)\n;; Returns: alist of (name . list-of-dep-names) for the full transitive closure\n\n(topological-sort graph)\n;; graph: alist of (node . (dep-node ...))\n;; Returns: list of nodes in dependency order (leaves first)\n```\n\n```scheme\n(define deps\n  (list (make-dep-spec 'web-server \"^1.0.0\" 'registry)\n        (make-dep-spec 'database   \"~2.3.0\" 'registry)))\n\n(define resolved (resolve-dependencies reg deps))\n;; resolved is a list of packages in build order\n(for-each (lambda (pkg)\n            (printf \"~a ~a~%\"\n              (package-name pkg)\n              (version->string (package-version pkg))))\n          resolved)\n```\n\n---\n\n## Lock File\n\nLock files record the exact resolved versions to ensure reproducible builds.\n\n```scheme\n(make-lock-file packages)   ; packages: list of (name . version-string) pairs\n(lock-file? x)\n\n(lock-file-write lf path)   ; write lock file to path\n(lock-file-read path)       ; read lock file from path; returns lock-file record\n```\n\n### Lock file format (`jerboa.lock`)\n\n```scheme\n;; Generated by jerboa pkg \u2014 do not edit\n((web-server . \"1.0.3\")\n (http-parser . \"2.1.4\")\n (database . \"2.3.1\"))\n```\n\n---\n\n## Package File\n\nThe `jerboa.pkg` file declares a package's metadata.\n\n```scheme\n(read-package-file path)    ; read jerboa.pkg \u2192 package record\n(write-package-file pkg path) ; write package record \u2192 jerboa.pkg\n```\n\n### `jerboa.pkg` format\n\n```scheme\n(package\n  (name my-app)\n  (version \"1.0.0\")\n  (description \"My application\")\n  (dependencies\n    (web-server \"^1.0.0\" registry)\n    (database   \"~2.3.0\" registry)\n    (utils      \">=0.5.0\" \"https://github.com/user/utils\")))\n```\n\n---\n\n## Complete Workflow\n\n```scheme\n(import (chezscheme) (jerboa pkg))\n\n;; 1. Create a registry with available packages\n(define reg (make-registry))\n(for-each (lambda (pkg) (registry-add! reg pkg))\n          (load-packages-from-server \"https://packages.jerboa-lang.org\"))\n\n;; 2. Read this project's package file\n(define this-pkg (read-package-file \"jerboa.pkg\"))\n\n;; 3. Resolve dependencies\n(define resolved\n  (resolve-dependencies reg (package-dependencies this-pkg)))\n\n(printf \"Resolved ~a dependencies:~%\" (length resolved))\n(for-each (lambda (p)\n            (printf \"  ~a ~a~%\"\n              (package-name p)\n              (version->string (package-version p))))\n          resolved)\n\n;; 4. Write lock file\n(lock-file-write\n  (make-lock-file\n    (map (lambda (p)\n           (cons (package-name p)\n                 (version->string (package-version p))))\n         resolved))\n  \"jerboa.lock\")\n\n(printf \"Lock file written.~%\")\n```\n"}
 {"text": "<!-- FILE: docs/cp0-passes.md -->\n# User-Defined cp0 Optimization Passes\n\n## Overview\n\nThe `(std compiler passes)` library exposes Chez Scheme's cp0 optimizer as a programmable interface, allowing users to write custom optimization passes that run during compilation.\n\n## Key Features\n\n### 1. Custom Pass Definition\n```scheme\n(import (std compiler passes))\n\n;; Define a pass using the simplified API\n(define-cp0-pass my-optimization-pass\n  \"Fold mathematical identities\"\n  (lambda (expr)\n    (match expr\n      [(list '+ x 0) x]\n      [(list '* x 1) x]  \n      [(list '* x 0) 0]\n      [_ #f]))  ; Return #f if no transformation applies\n  30)  ; Priority (lower runs first)\n```\n\n### 2. Pattern-Based Transformations\nThe passes use the advanced `match2` pattern matching system:\n\n```scheme\n;; Matrix operation fusion\n(define-cp0-pass matrix-fusion\n  \"Fuse consecutive matrix operations to avoid intermediate allocations\"\n  (lambda (expr)\n    (match expr\n      [(list 'matrix-* (list 'matrix-* a b) c)\n       (list 'matrix-*-fused a b c)]\n      [_ #f])))\n```\n\n### 3. Pass Registration and Management\n```scheme\n;; Register a pass with priority\n(register-optimization-pass! my-pass 25)\n\n;; List all registered passes\n(list-optimization-passes)\n;; => ((pass-name description priority enabled?) ...)\n\n;; Remove a pass\n(unregister-optimization-pass! 'my-pass)\n```\n\n### 4. Pass Composition\n```scheme\n;; Compose multiple passes into a pipeline\n(define my-pipeline\n  (compose-passes \n    pass:constant-fold\n    pass:dead-code-eliminate\n    my-optimization-pass))\n\n;; Apply composed passes\n(my-pipeline '(+ (* 2 3) 0))  ; => 6\n```\n\n### 5. Built-in Optimization Passes\n\nThe library provides several ready-to-use passes:\n\n- **`pass:constant-fold`** - Evaluates constant expressions at compile time\n- **`pass:dead-code-eliminate`** - Removes unreachable code and unused bindings  \n- **`pass:inline-small-functions`** - Inlines functions with small bodies\n- **`pass:loop-unroll`** - Unrolls loops with known small iteration counts\n\n### 6. Debugging Support\n```scheme\n;; Enable pass debugging\n(enable-pass-debug!)\n\n;; Check debug status\n(pass-debug-enabled?)  ; => #t\n\n;; Disable debugging\n(disable-pass-debug!)\n```\n\n## Domain-Specific Optimization Examples\n\n### SQL Query Optimization\n```scheme\n(define-cp0-pass sql-query-fusion\n  \"Combine consecutive SQL operations into single query\"\n  (lambda (expr)\n    (match expr\n      [(list 'sql-filter pred (list 'sql-map fn table))\n       (list 'sql-filter-map pred fn table)]\n      [(list 'sql-sort key (list 'sql-filter pred table))\n       (list 'sql-filter-sort pred key table)]\n      [_ #f])))\n```\n\n### Arithmetic Simplifications  \n```scheme\n(define-cp0-pass arithmetic-simplify\n  \"Simplify arithmetic expressions\"\n  (lambda (expr)\n    (match expr\n      [(list '+ x 0) x]\n      [(list '+ 0 x) x] \n      [(list '* x 1) x]\n      [(list '* 1 x) x]\n      [(list '* x 0) 0]\n      [(list '* 0 x) 0]\n      [(list '- x 0) x]\n      [(list '/ x 1) x]\n      [_ #f])))\n```\n\n## Usage Patterns\n\n### 1. Development Workflow\n```scheme\n;; 1. Define pass\n(define-cp0-pass my-pass \"...\" transformer)\n\n;; 2. Test pass individually  \n(let ([result ((cp0-pass-transformer my-pass) test-expr)])\n  (display result))\n\n;; 3. Register and test in pipeline\n(register-optimization-pass! my-pass)\n(apply-optimization-passes test-expr)\n```\n\n### 2. Performance Considerations\n- Passes run in priority order (lower numbers first)\n- Keep transformers fast - they run on every matching expression\n- Use guards to check expression structure before expensive operations\n- Return `#f` quickly for non-matching patterns\n\n### 3. Best Practices\n\n**Pattern Matching:**\n```scheme\n;; Good: Check structure first\n(lambda (expr)\n  (if (not (pair? expr))\n    #f  ; Fast exit for atoms\n    (match expr\n      [(list 'target-op args ...) (transform args)]\n      [_ #f])))\n```\n\n**Error Handling:**\n```scheme\n;; Good: Handle edge cases\n(lambda (expr)\n  (match expr\n    [(list '/ a b) \n     (if (and (number? a) (number? b) (not (= b 0)))\n       (/ a b)\n       #f)]  ; Don't divide by zero\n    [_ #f]))\n```\n\n## Integration with Chez Scheme\n\nThe passes integrate with Chez Scheme's compilation pipeline by:\n\n1. **AST Transformation** - Operating on s-expression representations\n2. **Type-Preserving** - Maintaining semantic equivalence  \n3. **Composable** - Can be combined with existing cp0 passes\n4. **Debuggable** - Providing introspection and logging\n\n## Performance Impact\n\n- **Compile-time Cost**: Passes add to compilation time proportional to code size\n- **Runtime Benefit**: Optimized code runs faster, reduced allocations\n- **Memory Usage**: Minimal - passes are stateless functions\n- **Scalability**: Linear in number of expressions processed\n\n## API Reference\n\n### Core Functions\n- `define-cp0-pass` - Define an optimization pass\n- `make-cp0-pass` - Create pass record manually\n- `register-optimization-pass!` - Add pass to global registry\n- `unregister-optimization-pass!` - Remove pass from registry\n- `list-optimization-passes` - List all registered passes\n- `apply-optimization-passes` - Apply all passes to expression\n- `compose-passes` - Combine multiple passes\n\n### Debugging\n- `enable-pass-debug!` - Enable debug output\n- `disable-pass-debug!` - Disable debug output  \n- `pass-debug-enabled?` - Check debug status\n- `dump-ir-between-passes!` - Enable IR dumping\n\n### Record Accessors\n- `cp0-pass?` - Test if object is a pass\n- `cp0-pass-name` - Get pass name\n- `cp0-pass-description` - Get pass description\n- `cp0-pass-transformer` - Get transformer function\n- `cp0-pass-priority` - Get/set pass priority\n- `cp0-pass-enabled` - Get/set enabled status\n\nThis implementation provides a powerful foundation for user-defined compiler optimizations while maintaining the safety and composability expected in a Scheme system."}
-{"text": "<!-- FILE: docs/compiling-gerbil-projects.md -->\n# Compiling Gerbil Projects with Jerboa\n\nThis guide walks through the complete process of taking an existing Gerbil\nScheme project and compiling it to run on Chez Scheme via Jerboa.\n\n## Prerequisites\n\n1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://github.com/ober/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```\n\n## Project Layout\n\nA jerboa project wraps an existing Gerbil project with build scripts and a\ncompatibility layer:\n\n```\nmy-jerboa-project/\n  my-gerbil-project/        # Gerbil source (submodule or copy)\n    gerbil.pkg\n    *.ss                     # Gerbil source files (never modified)\n  src/\n    compat/                  # Chez compatibility shims (handwritten)\n      sugar.sls              # :std/sugar equivalents\n      format.sls             # :std/format equivalents\n      ...\n    mylib/                   # Generated .sls files (gitignored)\n  build-jerboa.ss            # Jerboa translation driver\n  build-all.ss               # Chez compilation trigger\n  build-binary.ss            # Optional: standalone binary builder\n  main.ss                    # Entry point program\n  Makefile\n```\n\n## Step 1: Create the Makefile\n\n```makefile\nSCHEME = scheme\nJERBOA = $(or $(JERBOA_DIR),$(HOME)/src/jerboa/src)\nLIBDIRS = src:$(JERBOA)\nCOMPILE = $(SCHEME) -q --libdirs $(LIBDIRS) --compile-imported-libraries\n\n.PHONY: all jerboa compile binary run clean\n\nall: jerboa compile\n\n# Translate .ss -> .sls via jerboa\njerboa:\n\t$(COMPILE) < build-jerboa.ss\n\n# Compile .sls -> .so via Chez\ncompile: jerboa\n\t$(COMPILE) < build-all.ss\n\n# Build standalone binary\nbuild: binary\nbinary: clean jerboa\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program build-binary.ss\n\n# Run interpreted\nrun: all\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program main.ss\n\nclean:\n\tfind src -name '*.so' -o -name '*.wpo' | xargs rm -f 2>/dev/null || true\n\trm -f src/mylib/*.sls\n```\n\nThe `JERBOA` variable points to jerboa's `src/` directory. Override it:\n```bash\nJERBOA_DIR=/path/to/jerboa/src make\n```\n\nIf your project depends on other jerboa-compiled libraries, add them to\n`LIBDIRS`:\n```makefile\nJERBOA_AWS = $(or $(JERBOA_AWS_DIR),jerboa-aws/src)\nLIBDIRS = src:$(JERBOA):$(JERBOA_AWS)\n```\n\n## Step 2: Write the Import Map\n\nThe import map is the most important part. It tells jerboa how to translate\nGerbil's `:module/path` imports into R6RS `(library name)` imports.\n\n```scheme\n(define my-import-map\n  '(;; Standard library -> compat shims\n    (:std/sugar        . (compat sugar))\n    (:std/format       . (compat format))\n    (:std/sort         . (compat sort))\n    (:std/pregexp      . (compat pregexp))\n    (:std/error        . (runtime error))\n    (:std/misc/string  . (compat misc))\n    (:std/misc/list    . (compat misc))\n    (:std/misc/hash    . (compat misc))\n\n    ;; Strip imports that have no Chez equivalent\n    (:std/iter         . #f)   ;; jerboa compiles for-loops natively\n    (:std/foreign      . #f)   ;; no FFI at this level\n    (:gerbil/core      . #f)   ;; runtime provides these\n    (:gerbil/runtime   . #f)\n\n    ;; Project-internal imports (relative)\n    (\"./util\"          . (mylib util))\n\n    ;; Cross-project dependencies\n    (:ober/aws/s3      . (jerboa-aws s3-api))\n    ))\n```\n\nThree mapping types:\n- **`(library name)`**: Replace the import with this R6RS library\n- **`#f`**: Strip the import entirely (provided by base imports or unnecessary)\n- Unmapped imports cause a compile error \u2014 this is intentional, so you catch\n  missing mappings early\n\n## Step 3: Define Base Imports\n\nBase imports are injected into every compiled module. They provide the runtime\nenvironment that Gerbil code expects:\n\n```scheme\n(define my-base-imports\n  '(;; Chez Scheme with exclusions for Gambit-compatible replacements\n    (except (chezscheme) void box box? unbox set-box!\n            andmap ormap iota last-pair find\n            1+ 1- fx/ fx1+ fx1-\n            error error? raise with-exception-handler identifier?\n            hash-table? make-hash-table)\n    ;; Jerboa runtime\n    (compat types)\n    (runtime util)\n    (runtime table)\n    (runtime mop)\n    (runtime error)\n    (runtime hash)\n    ;; Gambit compatibility (threading, u8vectors, etc.)\n    (except (compat gambit) number->string make-mutex\n            with-output-to-string)))\n```\n\nThe `(except (chezscheme) ...)` clause is critical. Jerboa's runtime\nredefines several Chez builtins with Gambit-compatible versions. Without\nthe exclusions, R6RS reports import conflicts.\n\n## Step 4: Write build-jerboa.ss\n\nThe complete translation driver:\n\n```scheme\n#!chezscheme\n(import\n  (except (chezscheme) void box box? unbox set-box!\n          andmap ormap iota last-pair find\n          1+ 1- fx/ fx1+ fx1-\n          error error? raise with-exception-handler identifier?\n          hash-table? make-hash-table)\n  (compiler compile))\n\n(define output-dir \"src/mylib\")\n\n;; Import map and base imports (as defined above)\n(define my-import-map '(...))\n(define my-base-imports '(...))\n\n;; Compile one module\n(define (compile-module name)\n  (let* ((input  (string-append \"my-gerbil-project/\" name \".ss\"))\n         (output (string-append output-dir \"/\" name \".sls\"))\n         (lib    `(mylib ,(string->symbol name))))\n    (printf \"  Compiling ~a.ss~n\" name)\n    (guard (exn\n             (#t (printf \"  ERROR: ~a: ~a~n\" name (condition-message exn))\n                 #f))\n      (let* ((lib-form (gerbil-compile-to-library\n                         input lib my-import-map my-base-imports))\n             (lib-form (fix-import-conflicts lib-form)))\n        (call-with-output-file output\n          (lambda (port)\n            (display \"#!chezscheme\\n\" port)\n            (parameterize ([print-gensym #f])\n              (pretty-print lib-form port)))\n          'replace)\n        (printf \"  OK: ~a~n\" output)\n        #t))))\n\n;; Compile in dependency order (leaves first)\n(display \"=== Translating .ss -> .sls ===\\n\")\n(compile-module \"util\")\n(compile-module \"core\")\n(compile-module \"main\")\n\n(display \"=== Translation complete ===\\n\")\n```\n\n### Key functions from `(compiler compile)`:\n\n- **`gerbil-compile-to-library`** `(input-path lib-name import-map base-imports)`\n  Reads a `.ss` file, compiles it through jerboa, wraps in an R6RS library form.\n\n- **`fix-import-conflicts`** `(lib-form)`\n  Post-processes the library to add `(except ...)` for local definitions that\n  shadow imports. Also fixes `set!`'d exports with `identifier-syntax`.\n\n- **`jerboa-compile-file`** `(input-path)`\n  Lower-level: compiles a file and returns the compiled forms (no library wrapper).\n\n- **`jerboa-compile-string`** `(code-string)`\n  Compiles a string of Gerbil code. Useful for testing.\n\n## Step 5: Write build-all.ss\n\nThis simply imports every module, which triggers Chez's `--compile-imported-libraries`:\n\n```scheme\n#!chezscheme\n(import (mylib util) (mylib core) (mylib main))\n(printf \"All modules compiled.~n\")\n```\n\n## Step 6: Write Compatibility Shims\n\nYour project likely imports Gerbil standard library modules (`:std/sugar`,\n`:std/format`, etc.). These need Chez equivalents in `src/compat/`.\n\n### Example: src/compat/sugar.sls\n\n```scheme\n#!chezscheme\n(library (compat sugar)\n  (export try catch finally with-catch\n          hash hash-ref hash-set! hash-update! hash-keys hash-values\n          defstruct defrules)\n  (import (chezscheme))\n\n  (define-syntax try\n    (syntax-rules (catch finally)\n      [(_ body (catch (var) handler ...))\n       (guard (var [#t handler ...]) body)]\n      [(_ body (finally cleanup ...))\n       (dynamic-wind void (lambda () body) (lambda () cleanup ...))]))\n\n  ;; ... more shims\n  )\n```\n\n### Common Compat Modules\n\n| Gerbil Module | Chez Equivalent | What It Provides |\n|---------------|-----------------|------------------|\n| `:std/sugar` | `(compat sugar)` | `try/catch/finally`, `hash` literals, `with-catch` |\n| `:std/format` | `(compat format)` | `fprintf`, `printf`, `format` (Gambit-style) |\n| `:std/sort` | `(compat sort)` | `sort`, `stable-sort` (Gambit arg order) |\n| `:std/pregexp` | `(compat pregexp)` | Portable regex (pure Scheme) |\n| `:std/misc/*` | `(compat misc)` | `string-split`, `string-join`, `path-expand`, etc. |\n| `:std/error` | `(runtime error)` | `Error`, `error?`, `with-exception-catcher` |\n| `:std/os/signal` | `(compat signal)` | Signal handling |\n| `:std/os/fdio` | `(compat fdio)` | File descriptor I/O |\n\nYou don't always need to write these from scratch \u2014 many shims follow a common pattern and can be adapted.\n\n## Step 7: Build and Test\n\n```bash\nmake              # translate + compile\nmake run          # run interpreted\nmake build        # build standalone binary (if build-binary.ss exists)\n```\n\n## Common Issues and Fixes\n\n### Import Conflicts\n\n**Problem**: R6RS forbids a local `define` that shadows an imported name.\n\n**Fix**: `fix-import-conflicts` handles this automatically. If you see\n\"multiple definitions\" errors, ensure you're calling it on the library form\nbefore writing it out.\n\n### set! on Exported Variables\n\n**Problem**: R6RS forbids `set!` on exported variables.\n\n**Fix**: `fix-import-conflicts` rewrites these using `identifier-syntax` with\na vector cell:\n```scheme\n;; Before (forbidden in R6RS):\n(export foo)\n(define foo 0)\n(set! foo 42)\n\n;; After (automatic rewrite):\n(export foo)\n(define foo-cell (vector 0))\n(define-syntax foo\n  (identifier-syntax\n    (id (vector-ref foo-cell 0))\n    ((set! id v) (vector-set! foo-cell 0 v))))\n```\n\n### Keyword Arguments\n\n**Problem**: Gerbil uses `key: value` keyword arguments. Jerboa translates\nthese, but `defclass` constructors may pass keyword values as positional args.\n\n**Fix**: Post-build patch in `build-jerboa.ss`:\n```scheme\n(patch-file! \"src/mylib/types.sls\"\n  \"(lp (cddr rest) (cons (cadr rest) acc))]\"\n  \"(lp (cddr rest) acc)]\")\n```\n\n### make-mutex String Arguments\n\n**Problem**: Gambit's `make-mutex` accepts strings; Chez requires symbols.\n\n**Fix**: Post-build patch:\n```scheme\n(patch-file! \"src/mylib/foo.sls\"\n  \"(make-mutex \\\"my-lock\\\")\"\n  \"(make-mutex 'my-lock)\")\n```\n\n### void Recursion\n\n**Problem**: Gerbil defines `(define (void) ...)` which creates a recursive\ndefinition in the translated output.\n\n**Fix**: Post-build patch:\n```scheme\n(patch-file! \"src/mylib/foo.sls\"\n  \"(define (void) (void))\"\n  \"(define (void . _) (if #f #f))\")\n```\n\n### Chez Lazy Library Invocation\n\n**Problem**: Chez only invokes a library when one of its exports is first\nreferenced at runtime. If a module registers side effects at load time (like\nregistering builtins), they won't execute.\n\n**Fix**: Add a dummy reference in a module that's guaranteed to be invoked:\n```scheme\n;; In main.sls, force invocation of side-effecting modules:\n(define _force-builtins special-builtin?)  ;; references a builtins export\n```\n\n### Threading\n\n**Problem**: Programs in Chez boot files cannot create threads (GC futex\ndeadlock).\n\n**Fix**: Put only libraries in the boot file. Load the program at runtime via\n`memfd` + `Sscheme_script`. See [docs/single-binary.md](single-binary.md).\n\n## FFI (C Bindings)\n\nFor projects that need C FFI:\n\n1. Write a C shim (`ffi-shim.c`) with the functions you need\n2. Compile to a shared library: `gcc -shared -fPIC -o libffi.so ffi-shim.c`\n3. Create `src/compat/ffi.sls` using Chez's `foreign-procedure` and `load-shared-object`\n4. Map Gerbil's FFI imports to your compat module\n\nFor standalone binaries, the C shim is compiled directly into the binary. See\n[docs/single-binary.md](single-binary.md).\n\n## Optimization\n\nFor production builds:\n\n```bash\nmake compile-opt3     # optimize-level 3\nmake compile-wpo      # whole-program optimization\n```\n\nSee [docs/optimization.md](optimization.md) for tuning `cp0-effort-limit`,\nper-file directives, and benchmark results.\n\n## Reference: Existing Projects\n\nStudy these for real-world patterns:\n\n| Project | Modules | Key Patterns |\n|---------|---------|--------------|\n| jerboa-kunabi | 10+ | Cross-project deps (jerboa-aws), sed-based patches |\n| jerboa-lsp | 53 | Large module count, JSON/HTTP compat |\n"}
+{"text": "<!-- FILE: docs/compiling-gerbil-projects.md -->\n# Compiling Gerbil Projects with Jerboa\n\nThis guide walks through the complete process of taking an existing Gerbil\nScheme project and compiling it to run on Chez Scheme via Jerboa.\n\n## Prerequisites\n\n1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://git.sr.ht/~lisp/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```\n\n## Project Layout\n\nA jerboa project wraps an existing Gerbil project with build scripts and a\ncompatibility layer:\n\n```\nmy-jerboa-project/\n  my-gerbil-project/        # Gerbil source (submodule or copy)\n    gerbil.pkg\n    *.ss                     # Gerbil source files (never modified)\n  src/\n    compat/                  # Chez compatibility shims (handwritten)\n      sugar.sls              # :std/sugar equivalents\n      format.sls             # :std/format equivalents\n      ...\n    mylib/                   # Generated .sls files (gitignored)\n  build-jerboa.ss            # Jerboa translation driver\n  build-all.ss               # Chez compilation trigger\n  build-binary.ss            # Optional: standalone binary builder\n  main.ss                    # Entry point program\n  Makefile\n```\n\n## Step 1: Create the Makefile\n\n```makefile\nSCHEME = scheme\nJERBOA = $(or $(JERBOA_DIR),$(HOME)/src/jerboa/src)\nLIBDIRS = src:$(JERBOA)\nCOMPILE = $(SCHEME) -q --libdirs $(LIBDIRS) --compile-imported-libraries\n\n.PHONY: all jerboa compile binary run clean\n\nall: jerboa compile\n\n# Translate .ss -> .sls via jerboa\njerboa:\n\t$(COMPILE) < build-jerboa.ss\n\n# Compile .sls -> .so via Chez\ncompile: jerboa\n\t$(COMPILE) < build-all.ss\n\n# Build standalone binary\nbuild: binary\nbinary: clean jerboa\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program build-binary.ss\n\n# Run interpreted\nrun: all\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program main.ss\n\nclean:\n\tfind src -name '*.so' -o -name '*.wpo' | xargs rm -f 2>/dev/null || true\n\trm -f src/mylib/*.sls\n```\n\nThe `JERBOA` variable points to jerboa's `src/` directory. Override it:\n```bash\nJERBOA_DIR=/path/to/jerboa/src make\n```\n\nIf your project depends on other jerboa-compiled libraries, add them to\n`LIBDIRS`:\n```makefile\nJERBOA_AWS = $(or $(JERBOA_AWS_DIR),jerboa-aws/src)\nLIBDIRS = src:$(JERBOA):$(JERBOA_AWS)\n```\n\n## Step 2: Write the Import Map\n\nThe import map is the most important part. It tells jerboa how to translate\nGerbil's `:module/path` imports into R6RS `(library name)` imports.\n\n```scheme\n(define my-import-map\n  '(;; Standard library -> compat shims\n    (:std/sugar        . (compat sugar))\n    (:std/format       . (compat format))\n    (:std/sort         . (compat sort))\n    (:std/pregexp      . (compat pregexp))\n    (:std/error        . (runtime error))\n    (:std/misc/string  . (compat misc))\n    (:std/misc/list    . (compat misc))\n    (:std/misc/hash    . (compat misc))\n\n    ;; Strip imports that have no Chez equivalent\n    (:std/iter         . #f)   ;; jerboa compiles for-loops natively\n    (:std/foreign      . #f)   ;; no FFI at this level\n    (:gerbil/core      . #f)   ;; runtime provides these\n    (:gerbil/runtime   . #f)\n\n    ;; Project-internal imports (relative)\n    (\"./util\"          . (mylib util))\n\n    ;; Cross-project dependencies\n    (:ober/aws/s3      . (jerboa-aws s3-api))\n    ))\n```\n\nThree mapping types:\n- **`(library name)`**: Replace the import with this R6RS library\n- **`#f`**: Strip the import entirely (provided by base imports or unnecessary)\n- Unmapped imports cause a compile error \u2014 this is intentional, so you catch\n  missing mappings early\n\n## Step 3: Define Base Imports\n\nBase imports are injected into every compiled module. They provide the runtime\nenvironment that Gerbil code expects:\n\n```scheme\n(define my-base-imports\n  '(;; Chez Scheme with exclusions for Gambit-compatible replacements\n    (except (chezscheme) void box box? unbox set-box!\n            andmap ormap iota last-pair find\n            1+ 1- fx/ fx1+ fx1-\n            error error? raise with-exception-handler identifier?\n            hash-table? make-hash-table)\n    ;; Jerboa runtime\n    (compat types)\n    (runtime util)\n    (runtime table)\n    (runtime mop)\n    (runtime error)\n    (runtime hash)\n    ;; Gambit compatibility (threading, u8vectors, etc.)\n    (except (compat gambit) number->string make-mutex\n            with-output-to-string)))\n```\n\nThe `(except (chezscheme) ...)` clause is critical. Jerboa's runtime\nredefines several Chez builtins with Gambit-compatible versions. Without\nthe exclusions, R6RS reports import conflicts.\n\n## Step 4: Write build-jerboa.ss\n\nThe complete translation driver:\n\n```scheme\n#!chezscheme\n(import\n  (except (chezscheme) void box box? unbox set-box!\n          andmap ormap iota last-pair find\n          1+ 1- fx/ fx1+ fx1-\n          error error? raise with-exception-handler identifier?\n          hash-table? make-hash-table)\n  (compiler compile))\n\n(define output-dir \"src/mylib\")\n\n;; Import map and base imports (as defined above)\n(define my-import-map '(...))\n(define my-base-imports '(...))\n\n;; Compile one module\n(define (compile-module name)\n  (let* ((input  (string-append \"my-gerbil-project/\" name \".ss\"))\n         (output (string-append output-dir \"/\" name \".sls\"))\n         (lib    `(mylib ,(string->symbol name))))\n    (printf \"  Compiling ~a.ss~n\" name)\n    (guard (exn\n             (#t (printf \"  ERROR: ~a: ~a~n\" name (condition-message exn))\n                 #f))\n      (let* ((lib-form (gerbil-compile-to-library\n                         input lib my-import-map my-base-imports))\n             (lib-form (fix-import-conflicts lib-form)))\n        (call-with-output-file output\n          (lambda (port)\n            (display \"#!chezscheme\\n\" port)\n            (parameterize ([print-gensym #f])\n              (pretty-print lib-form port)))\n          'replace)\n        (printf \"  OK: ~a~n\" output)\n        #t))))\n\n;; Compile in dependency order (leaves first)\n(display \"=== Translating .ss -> .sls ===\\n\")\n(compile-module \"util\")\n(compile-module \"core\")\n(compile-module \"main\")\n\n(display \"=== Translation complete ===\\n\")\n```\n\n### Key functions from `(compiler compile)`:\n\n- **`gerbil-compile-to-library`** `(input-path lib-name import-map base-imports)`\n  Reads a `.ss` file, compiles it through jerboa, wraps in an R6RS library form.\n\n- **`fix-import-conflicts`** `(lib-form)`\n  Post-processes the library to add `(except ...)` for local definitions that\n  shadow imports. Also fixes `set!`'d exports with `identifier-syntax`.\n\n- **`jerboa-compile-file`** `(input-path)`\n  Lower-level: compiles a file and returns the compiled forms (no library wrapper).\n\n- **`jerboa-compile-string`** `(code-string)`\n  Compiles a string of Gerbil code. Useful for testing.\n\n## Step 5: Write build-all.ss\n\nThis simply imports every module, which triggers Chez's `--compile-imported-libraries`:\n\n```scheme\n#!chezscheme\n(import (mylib util) (mylib core) (mylib main))\n(printf \"All modules compiled.~n\")\n```\n\n## Step 6: Write Compatibility Shims\n\nYour project likely imports Gerbil standard library modules (`:std/sugar`,\n`:std/format`, etc.). These need Chez equivalents in `src/compat/`.\n\n### Example: src/compat/sugar.sls\n\n```scheme\n#!chezscheme\n(library (compat sugar)\n  (export try catch finally with-catch\n          hash hash-ref hash-set! hash-update! hash-keys hash-values\n          defstruct defrules)\n  (import (chezscheme))\n\n  (define-syntax try\n    (syntax-rules (catch finally)\n      [(_ body (catch (var) handler ...))\n       (guard (var [#t handler ...]) body)]\n      [(_ body (finally cleanup ...))\n       (dynamic-wind void (lambda () body) (lambda () cleanup ...))]))\n\n  ;; ... more shims\n  )\n```\n\n### Common Compat Modules\n\n| Gerbil Module | Chez Equivalent | What It Provides |\n|---------------|-----------------|------------------|\n| `:std/sugar` | `(compat sugar)` | `try/catch/finally`, `hash` literals, `with-catch` |\n| `:std/format` | `(compat format)` | `fprintf`, `printf`, `format` (Gambit-style) |\n| `:std/sort` | `(compat sort)` | `sort`, `stable-sort` (Gambit arg order) |\n| `:std/pregexp` | `(compat pregexp)` | Portable regex (pure Scheme) |\n| `:std/misc/*` | `(compat misc)` | `string-split`, `string-join`, `path-expand`, etc. |\n| `:std/error` | `(runtime error)` | `Error`, `error?`, `with-exception-catcher` |\n| `:std/os/signal` | `(compat signal)` | Signal handling |\n| `:std/os/fdio` | `(compat fdio)` | File descriptor I/O |\n\nYou don't always need to write these from scratch \u2014 many shims follow a common pattern and can be adapted.\n\n## Step 7: Build and Test\n\n```bash\nmake              # translate + compile\nmake run          # run interpreted\nmake build        # build standalone binary (if build-binary.ss exists)\n```\n\n## Common Issues and Fixes\n\n### Import Conflicts\n\n**Problem**: R6RS forbids a local `define` that shadows an imported name.\n\n**Fix**: `fix-import-conflicts` handles this automatically. If you see\n\"multiple definitions\" errors, ensure you're calling it on the library form\nbefore writing it out.\n\n### set! on Exported Variables\n\n**Problem**: R6RS forbids `set!` on exported variables.\n\n**Fix**: `fix-import-conflicts` rewrites these using `identifier-syntax` with\na vector cell:\n```scheme\n;; Before (forbidden in R6RS):\n(export foo)\n(define foo 0)\n(set! foo 42)\n\n;; After (automatic rewrite):\n(export foo)\n(define foo-cell (vector 0))\n(define-syntax foo\n  (identifier-syntax\n    (id (vector-ref foo-cell 0))\n    ((set! id v) (vector-set! foo-cell 0 v))))\n```\n\n### Keyword Arguments\n\n**Problem**: Gerbil uses `key: value` keyword arguments. Jerboa translates\nthese, but `defclass` constructors may pass keyword values as positional args.\n\n**Fix**: Post-build patch in `build-jerboa.ss`:\n```scheme\n(patch-file! \"src/mylib/types.sls\"\n  \"(lp (cddr rest) (cons (cadr rest) acc))]\"\n  \"(lp (cddr rest) acc)]\")\n```\n\n### make-mutex String Arguments\n\n**Problem**: Gambit's `make-mutex` accepts strings; Chez requires symbols.\n\n**Fix**: Post-build patch:\n```scheme\n(patch-file! \"src/mylib/foo.sls\"\n  \"(make-mutex \\\"my-lock\\\")\"\n  \"(make-mutex 'my-lock)\")\n```\n\n### void Recursion\n\n**Problem**: Gerbil defines `(define (void) ...)` which creates a recursive\ndefinition in the translated output.\n\n**Fix**: Post-build patch:\n```scheme\n(patch-file! \"src/mylib/foo.sls\"\n  \"(define (void) (void))\"\n  \"(define (void . _) (if #f #f))\")\n```\n\n### Chez Lazy Library Invocation\n\n**Problem**: Chez only invokes a library when one of its exports is first\nreferenced at runtime. If a module registers side effects at load time (like\nregistering builtins), they won't execute.\n\n**Fix**: Add a dummy reference in a module that's guaranteed to be invoked:\n```scheme\n;; In main.sls, force invocation of side-effecting modules:\n(define _force-builtins special-builtin?)  ;; references a builtins export\n```\n\n### Threading\n\n**Problem**: Programs in Chez boot files cannot create threads (GC futex\ndeadlock).\n\n**Fix**: Put only libraries in the boot file. Load the program at runtime via\n`memfd` + `Sscheme_script`. See [docs/single-binary.md](single-binary.md).\n\n## FFI (C Bindings)\n\nFor projects that need C FFI:\n\n1. Write a C shim (`ffi-shim.c`) with the functions you need\n2. Compile to a shared library: `gcc -shared -fPIC -o libffi.so ffi-shim.c`\n3. Create `src/compat/ffi.sls` using Chez's `foreign-procedure` and `load-shared-object`\n4. Map Gerbil's FFI imports to your compat module\n\nFor standalone binaries, the C shim is compiled directly into the binary. See\n[docs/single-binary.md](single-binary.md).\n\n## Optimization\n\nFor production builds:\n\n```bash\nmake compile-opt3     # optimize-level 3\nmake compile-wpo      # whole-program optimization\n```\n\nSee [docs/optimization.md](optimization.md) for tuning `cp0-effort-limit`,\nper-file directives, and benchmark results.\n\n## Reference: Existing Projects\n\nStudy these for real-world patterns:\n\n| Project | Modules | Key Patterns |\n|---------|---------|--------------|\n| jerboa-kunabi | 10+ | Cross-project deps (jerboa-aws), sed-based patches |\n| jerboa-lsp | 53 | Large module count, JSON/HTTP compat |\n"}
 {"text": "<!-- FILE: docs/optimization.md -->\n# Chez Scheme Optimization Techniques for Jerboa\n\nTechniques from the Chez Scheme compiler (v10.4.0) that can improve performance of Gerbil-to-Chez converted code.\n\n## 1. Compiler Parameter Tuning\n\nThe most impactful knobs for compiled Jerboa output:\n\n| Parameter | Default | Recommendation |\n|-----------|---------|----------------|\n| `optimize-level` | 2 | Use 3 for well-tested code (removes all type checks) |\n| `cp0-effort-limit` | 200 | Increase to 500+ for generated code with deep nesting |\n| `cp0-score-limit` | 20 | Increase to 50 to allow more aggressive inlining |\n| `enable-type-recovery` | #t | Keep enabled \u2014 helps recover types after Gerbil wrapping layers |\n| `enable-cross-library-optimization` | #t | Critical since Jerboa emits many small R6RS libraries |\n\nSet these per-file in the compilation pipeline via `(eval-when (compile) ...)` for hot modules.\n\n## 2. Record Type Flags\n\nWhen compiling `defstruct`/`defclass` to Chez records, emit:\n\n- **`(sealed #t)`** \u2014 prevents subclassing, enables direct field access without vtable indirection\n- **`(immutable)` on fields** where Gerbil code never uses `set!` on slots \u2014 cp0 can then fold field accesses\n- **`(nongenerative <uid>)`** \u2014 enables cross-library type identity and optimization\n\nCurrently the MOP uses `gerbil-struct` wrappers. For performance-critical structs, consider emitting native Chez `define-record-type` directly with these flags.\n\n## 3. Numeric Specialization\n\nChez unboxes flonums at level 3 but only with explicit flonum ops:\n\n```scheme\n;; Generic (boxes intermediate results, GC pressure)\n(+ (* x y) z)\n\n;; Specialized (raw FPU instructions, no allocation)\n(fl+ (fl* x y) z)\n```\n\nThe compiler could emit `fx+`/`fl+` when type annotations are available from Gerbil's `def` forms (e.g., `(def (f (x : fixnum)) ...)`).\n\n## 4. Local Binding Optimization\n\nChez's cp0 aggressively inlines **locally-bound, unassigned procedures**. Top-level `define`s across library boundaries are harder to optimize. This matters because Jerboa emits many small libraries.\n\n**Actionable**: When a Gerbil helper function is only used in one place, emit it as a local `let` binding rather than a top-level `define`.\n\n## 5. Type Guard Patterns\n\ncp0 + cptypes recognize predicate-guarded branches:\n\n```scheme\n;; cptypes knows x is a pair in the consequent\n(if (pair? x) (car x) ...)\n\n;; cptypes knows x is a fixnum\n(if (fixnum? x) (fx+ x 1) ...)\n```\n\nThe `match` compiler already emits type tests \u2014 make sure the pattern is `(if (predicate? x) <then> <else>)` rather than intermediate wrappers that obscure the type information from cptypes.\n\n## 6. Avoid the WPO Trap\n\nWPO breaks `identifier-syntax` mutable export cells. But for pure-Scheme modules without mutable exports, WPO could still be applied selectively. Consider a build flag that enables WPO per-module.\n\n## 7. Letrec Decomposition\n\nChez's `cpletrec` pass decomposes `letrec*` into simpler forms. Since Gerbil's top-level `def` forms compile to `letrec*` bodies in R6RS libraries, this pass is already working for you. But ordering matters \u2014 put pure definitions before impure ones to help cpletrec separate them.\n\n## 8. Primitive Inlining Flags\n\nEach Chez primitive has optimization flags (`pure`, `unrestricted`, `cp02`, `arith-op`, etc.) in `primdata.ss`. When the compat layer wraps Chez primitives (e.g., `hash-ref` wrapping `hashtable-ref`), the wrapper obscures these flags from the optimizer.\n\n**Actionable**: For hot-path compat wrappers, consider:\n- Using `define-syntax` + `syntax-rules` instead of `define` to make them transparent to cp0\n- Or mark them with integration info if applicable\n\n## 9. Closure Avoidance in Loops\n\nChez doesn't optimize closures allocated inside loops. The `for`/`for/collect` compilation should ensure lambda bodies are hoisted when they don't capture loop variables.\n\n## 10. Quick Wins Checklist\n\n- [ ] Add `(optimize-level 3)` option to gxc for release builds\n- [ ] Emit `sealed` records for `defstruct` without known subclasses\n- [ ] Emit `immutable` fields for non-mutated struct slots\n- [ ] Emit `fx+`/`fx-` for integer arithmetic in type-annotated code\n- [ ] Ensure `match` type tests are direct `(if (pred? x) ...)` \u2014 not wrapped\n- [ ] Increase `cp0-effort-limit` for generated code (deep nesting from macro expansion)\n- [ ] Consider selective WPO for pure modules without mutable exports\n- [ ] Profile the compat wrappers (hash, MOP dispatch) \u2014 these are likely the hottest paths\n\n---\n\n## 11. Chez Scheme Optimization Functions & APIs\n\n### `expand/optimize` \u2014 Inspect Optimizer Output\n\nUse this to see what Chez's optimizer actually does with generated code. Invaluable for checking whether compat wrappers are being inlined or blocking optimization:\n\n```scheme\n(expand/optimize '(let ([y '(3 . 4)]) (+ (car y) (cdr y))))\n;=> 7   (fully folded at compile time)\n\n(print-gensym #f)\n(expand/optimize\n  '(let ([y '(3 . 4)])\n     (lambda (x) (* (+ (car y) (cdr y)) x))))\n;=> (lambda (x) (#2%* 7 x))\n```\n\n### `compile-whole-program` \u2014 Whole-Program Optimization\n\nThe most aggressive optimization Chez offers. Discards unused code and optimizes across all library boundaries:\n\n```scheme\n(parameterize ([generate-wpo-files #t])\n  (compile-program \"main.ss\" \"main.so\"))\n(compile-whole-program \"main.wpo\" \"main-opt.so\")\n```\n\nReturns a list of libraries that couldn't be incorporated (missing `.wpo` files). Breaks `identifier-syntax` mutable export cells \u2014 use only for modules without mutable exports.\n\n### `compile-whole-library` \u2014 Same for Libraries\n\nLike `compile-whole-program` but for library bundles. Produces both `.so` and `.wpo` output.\n\n### `$primitive` / `#%` Syntax \u2014 Force Original Primitives\n\nBypass any user redefinitions and control per-call optimization level:\n\n```scheme\n(#3%car x)     ; car at optimize-level 3 (no type check)\n(#2%+ a b)     ; + at optimize-level 2 (with checks)\n(#%vector-ref v i)  ; original primitive, current optimize level\n```\n\n## 12. Complete Parameter Reference\n\n| Parameter | Default | Effect |\n|-----------|---------|--------|\n| `optimize-level` | 0 | 0-2 = safe; **3 = unsafe** (no type/bounds checks) |\n| `run-cp0` | 2-pass | Controls the source optimizer; can disable or run N passes |\n| `cp0-effort-limit` | 200 | Max work per inlining attempt |\n| `cp0-score-limit` | 20 | Max code growth per inlining |\n| `cp0-outer-unroll-limit` | 0 | Loop unrolling (set to 1+ to enable) |\n| `enable-type-recovery` | #t | cptypes pass \u2014 removes redundant type checks |\n| `enable-cross-library-optimization` | #t | Propagate constants/inlining across libraries |\n| `enable-unsafe-application` | #f | Skip procedure? check on calls (even below level 3) |\n| `enable-unsafe-variable-reference` | #f | Skip letrec undefined-var checks |\n| `enable-arithmetic-left-associative` | #f | Allow arithmetic reassociation |\n| `commonization-level` | 0 | 0-9; merges structurally similar lambdas (reduces code size) |\n| `generate-wpo-files` | #f | Emit `.wpo` files for whole-program optimization |\n| `generate-inspector-information` | #t | Set #f to reduce object file size |\n| `debug-level` | 1 | 0 = max optimization, 3 = max debuggability |\n| `compile-interpret-simple` | #t | Interpret simple expressions instead of compiling |\n\n## 13. Customizing the cp0 Pass\n\nThe default runs cp0 **twice** (second pass catches things the first missed after inlining). Customizable via `run-cp0`:\n\n```scheme\n;; Disable cp0 entirely (fastest compile, no optimization)\n(run-cp0 (lambda (cp0 x) x))\n\n;; Run once (faster compile, slightly less optimization)\n(run-cp0 (lambda (cp0 x) (cp0 x)))\n\n;; Run 3 times (slower compile, more aggressive)\n(run-cp0 (lambda (cp0 x) (cp0 (cp0 (cp0 x)))))\n```\n\n## 14. Commonization \u2014 Code Size Reduction for Generated Code\n\nThe `commonization-level` parameter (0-9) merges structurally similar lambda expressions by abstracting differences at leaf positions (constants, variable references, primitive references).\n\n```scheme\n(eval-when (compile) (commonization-level 5))\n```\n\n**Especially relevant for Jerboa**: the compiler generates many structurally similar lambdas (match arms, method dispatchers, accessor functions). Setting this to 3-5 could significantly reduce code size and improve instruction cache utilization. Trade-off: can undo some cp0 inlining and complicate debugging.\n\n## 15. Per-File Optimization Directives\n\nApply optimization settings to individual files without affecting the rest of the build:\n\n```scheme\n(eval-when (compile)\n  (optimize-level 3)\n  (cp0-effort-limit 500)\n  (generate-inspector-information #f)\n  (commonization-level 3))\n```\n\nThis only affects compilation (via `compile-file`), not source loading.\n\n## 16. Recommendations Specific to Jerboa\n\n1. **`expand/optimize`** \u2014 Use to inspect whether compat wrappers (hash-ref, slot-ref) are being inlined or blocking optimization. If they're opaque, convert to `define-syntax` wrappers.\n\n2. **`commonization-level` 3-5** \u2014 High value for Jerboa since the compiler generates many structurally similar lambdas from match, defmethod, and accessor compilation.\n\n3. **`compile-whole-program`** \u2014 For modules without mutable exports, this gives the biggest wins. Worth investigating per-module WPO enablement.\n\n4. **`enable-unsafe-application`** \u2014 A middle ground: skip the \"is this a procedure?\" check without going full optimize-level 3. Safe for generated code where every call target is known to be a procedure.\n\n5. **`generate-inspector-information #f`** \u2014 For release builds, disabling this reduces `.so` file sizes.\n\n6. **`debug-level 0`** \u2014 For release builds, allows maximum continuation optimization.\n\n## 17. Empirical Results (gherkin-shell benchmarks)\n\nTested on gherkin-shell using shellbench. All numbers are executions/second (higher = better).\n\n| Optimization | Avg Improvement | Best Test | Binary Size |\n|-------------|----------------|-----------|-------------|\n| Baseline (default) | \u2014 | \u2014 | 6,543 KB |\n| optimize-level 3 (shell only) | +3.2% | +6.3% | 6,499 KB |\n| opt3 (runtime + shell) | +5.2% | +6.4% | 6,475 KB |\n| opt3 + tuned cp0 + no inspector | +5.0% | +6.3% | 6,003 KB (-8.3%) |\n| opt3 + tuned cp0 + partial WPO | +8.7% | +16.3% | 7,075 KB |\n| **opt3 + tuned cp0 + full WPO** | **+9.6%** | **+20.3%** | 6,891 KB (+5.3%) |\n\nKey findings:\n- **WPO works** despite `identifier-syntax` mutable exports (they survived WPO in this project)\n- **Full WPO requires `.wpo` files** from both the Jerboa runtime and application compat layers\n- **cp0 tuning** (effort 500, score 50) adds ~1-2% on top of opt3 alone\n- **`generate-inspector-information #f`** reduces binary size significantly (-8.3%) with negligible performance impact\n- **Biggest wins** on comparison operations (`cmp: [ ]` +20.3%) and arithmetic (`count: typeset -i` +17.9%)\n\n## Summary\n\nThe biggest gains will come from **optimize-level 3** (+3-5%), **WPO** (+4-5% additional), and **tuned cp0 limits** (+1-2%). Record type flags (sealed/immutable) and commonization remain untested but should provide further gains. Use `expand/optimize` to verify that optimizations are actually taking effect on generated code.\n"}
 {"text": "<!-- FILE: docs/fix-fibers.md -->\n# Fiber Gaps \u2014 Implementation Plan\n\nStatus: **draft** (2026-04-11)\n\nThe `(std fiber)` core (668 lines, 18 tests) is solid: M:N scheduling, engine-based preemption, cooperative yield, fiber-aware channels, sleep with timer queue. This document covers the gaps and how to fill them, ordered by value.\n\n---\n\n## 1. Fix `cpu-count` (trivial)\n\n**Gap**: `fiber.sls:221-222` hardcodes `(if (threaded?) 4 1)`.\n\n**Fix**: Reuse the real detection from `actor/scheduler.sls:135-146`, which reads `/proc/cpuinfo` with a fallback to 4.\n\nOptions:\n- (a) Factor the scheduler's `cpu-count` into a shared `(std misc cpu)` module that both import.\n- (b) Inline the same `/proc/cpuinfo` reader into `fiber.sls`.\n\nPrefer (a) \u2014 one source of truth, and other modules (engine pool, task groups) can use it too.\n\n**Effort**: ~30 minutes. **Tests**: Verify `(make-fiber-runtime)` picks up actual core count.\n\n---\n\n## 2. Fiber-local storage\n\n**Gap**: No per-fiber variable bindings. `current-fiber` and `current-fiber-runtime` are `make-thread-parameter`, which is OS-thread-scoped. Since multiple fibers share one OS worker thread, a fiber-local needs its own mechanism.\n\n**Design**: A `fiber-parameter` that stores values keyed by fiber ID.\n\n```scheme\n(define (make-fiber-parameter default)\n  (let ([store (make-eq-hashtable)]\n        [mx (make-mutex)])\n    (case-lambda\n      [()    ;; read\n       (let ([f (current-fiber)])\n         (if f\n           (begin (mutex-acquire mx)\n                  (let ([v (hashtable-ref store (fiber-id f) default)])\n                    (mutex-release mx) v))\n           default))]\n      [(val) ;; write\n       (let ([f (current-fiber)])\n         (unless f (error 'fiber-parameter \"not in a fiber\"))\n         (mutex-acquire mx)\n         (hashtable-set! store (fiber-id f) val)\n         (mutex-release mx))])))\n```\n\n**Cleanup**: When a fiber completes (`mark-fiber-done!`), sweep its entries from all registered fiber-parameters. Keep a global weak list of all live fiber-parameters, or add a per-fiber cleanup hook list.\n\n**Convenience macro**:\n```scheme\n(define-syntax fiber-parameterize\n  (syntax-rules ()\n    [(_ ([fp val] ...) body ...)\n     (let ([old-fp (fp)] ...)\n       (dynamic-wind\n         (lambda () (fp val) ...)\n         (lambda () body ...)\n         (lambda () (fp old-fp) ...)))]))\n```\n\n**Effort**: ~2 hours. **Tests**: fiber-local isolation across concurrent fibers on same worker thread, cleanup after fiber completion.\n\n---\n\n## 3. Fiber cancellation\n\n**Gap**: No way to cancel a running fiber from outside. Once spawned, a fiber runs until its thunk returns or raises.\n\n**Design**: Cooperative cancellation via a cancel token (same pattern as `task.sls:36-53`).\n\nAdd to the fiber record:\n```scheme\n(mutable cancelled?)   ;; boolean, checked at yield/sleep/channel-wait\n```\n\nAPI:\n```scheme\n(fiber-cancel! f)          ;; set cancelled? flag, wake if parked\n(fiber-cancelled? f)       ;; check flag\n(fiber-check-cancelled!)   ;; called inside fiber \u2014 raises &fiber-cancelled if set\n```\n\n**Cancellation points**: `fiber-yield`, `fiber-sleep`, `fiber-channel-recv`, and `fiber-channel-send` all check the cancelled flag before parking. Raise `&fiber-cancelled` condition.\n\n**Force-cancel after timeout**: Like the actor supervisor's graceful shutdown (`supervisor.sls:215-240`), allow a deadline after which the fiber's engine is simply not resumed.\n\n```scheme\n(fiber-cancel! f)                ;; cooperative \u2014 sets flag\n(fiber-cancel! f timeout-ms)     ;; cooperative, then force-abandon after timeout\n```\n\n**Condition type**:\n```scheme\n(define-condition-type &fiber-cancelled &serious\n  make-fiber-cancelled fiber-cancelled?\n  (fiber-id cancelled-fiber-id))\n```\n\n**Effort**: ~3 hours. **Tests**: cancel parked fiber, cancel running fiber at yield point, cancel with timeout, double-cancel idempotent.\n\n---\n\n## 4. Error propagation\n\n**Gap**: Exceptions in a fiber are silently captured in `fiber-result` (`fiber.sls:436-441`). No notification to parent or supervisor.\n\n**Design**: Two mechanisms, matching the actor system's patterns.\n\n### 4a. fiber-join (blocking result retrieval)\n\n```scheme\n(fiber-join f)             ;; block current fiber until f completes, return result\n(fiber-join f timeout-ms)  ;; with timeout, raises &fiber-timeout on expiry\n```\n\nIf `f` completed with an exception, `fiber-join` re-raises it in the joining fiber. Implementation: add a \"join-waiters\" list to the fiber record; `mark-fiber-done!` wakes them.\n\n### 4b. fiber-link (Erlang-style crash propagation)\n\n```scheme\n(fiber-link! f)      ;; link current fiber to f \u2014 if f dies with error, current fiber gets &fiber-linked-crash\n(fiber-unlink! f)\n```\n\nWhen a linked fiber crashes, all linked fibers receive a `&fiber-linked-crash` condition at their next cancellation point. Simpler than full OTP monitors but covers the \"if my child dies, I die\" use case.\n\n**Condition types**:\n```scheme\n(define-condition-type &fiber-timeout &serious\n  make-fiber-timeout fiber-timeout?\n  (fiber-id timeout-fiber-id))\n\n(define-condition-type &fiber-linked-crash &serious\n  make-fiber-linked-crash fiber-linked-crash?\n  (source-fiber-id linked-crash-source)\n  (original-condition linked-crash-condition))\n```\n\n**Effort**: 4a ~2 hours, 4b ~3 hours. **Tests**: join on completed fiber, join on crashed fiber re-raises, join timeout, link propagation.\n\n---\n\n## 5. Fiber-channel select (`fiber-select`)\n\n**Gap**: No way to wait on multiple fiber-channels simultaneously.\n\n**Design**: Adapt the CSP select spin-poll pattern (`csp/select.sls:166-174`) for fiber-channels.\n\n```scheme\n(fiber-select\n  [ch1 val => (handle-val val)]           ;; recv from ch1\n  [ch2 :send msg => (handle-sent)]        ;; send msg to ch2\n  [:timeout 5000 => (handle-timeout)]     ;; optional timeout clause\n  [:default => (handle-none)])            ;; optional non-blocking clause\n```\n\n**Implementation**: Macro expands to a loop that:\n1. Try each clause's channel with `fiber-channel-try-recv` / `fiber-channel-try-send`.\n2. If any succeeds, evaluate its body and return.\n3. If `:default` clause exists and nothing ready, run it.\n4. Otherwise, park the fiber on all channels' waiter lists, use a shared gate so that whichever channel fires first wakes the fiber.\n\nThe shared-gate approach avoids the spin-poll cost: register the fiber on multiple channels, first one to wake it wins, others remove the stale waiter entry.\n\n**Alternative**: Integrate with `(std event)` by providing `fiber-recv-evt` / `fiber-send-evt` that return event objects compatible with `choice` and `sync`. This is cleaner but couples the two modules.\n\nRecommend: start with the macro (self-contained in `(std fiber)`), add event integration later.\n\n**Effort**: ~4 hours. **Tests**: select across 2+ channels, select with timeout, select with default, select with send+recv mix.\n\n---\n\n## 6. Structured concurrency (`with-fiber-group`)\n\n**Gap**: No scoped lifecycle management. Fibers spawned with `fiber-spawn*` are fire-and-forget; no automatic cleanup, no \"wait for all children\" scope.\n\n**Design**: Follows the pattern from `task.sls` and `concur/structured.sls`.\n\n```scheme\n(with-fiber-group\n  (lambda (group)\n    (fiber-group-spawn group (lambda () (do-work-a)))\n    (fiber-group-spawn group (lambda () (do-work-b)))\n    ;; implicit: waits for all children to complete\n    ;; if any child raises, cancels siblings, re-raises in parent\n    ))\n```\n\n**Semantics**:\n- `with-fiber-group` creates a group, evaluates the body, then blocks until all spawned fibers complete.\n- If any fiber in the group raises an unhandled exception, all other fibers in the group are cancelled (cooperative), and the exception is re-raised in the calling fiber.\n- If the calling fiber is itself cancelled, all children are cancelled.\n- Cleanup is guaranteed via `dynamic-wind`.\n\n**Group record**:\n```scheme\n(define-record-type fiber-group\n  (fields\n    (mutable fibers)        ;; list of child fibers\n    (mutable first-exn)     ;; first exception, or #f\n    (mutable cancelled?)\n    (immutable mutex)\n    (immutable all-done)    ;; condition variable\n    (mutable done-count)\n    (mutable total-count)))\n```\n\n**Effort**: ~4 hours. **Tests**: all-succeed, first-error-cancels-rest, parent-cancel-propagates, nested groups.\n\n---\n\n## 7. Fiber-aware timeouts\n\n**Gap**: `fiber-sleep` parks for a duration, but there's no \"do X or timeout\" pattern.\n\n**Design**: Build on `fiber-select` (gap 5) plus a timeout channel.\n\n```scheme\n(define (fiber-timeout ms)\n  ;; Returns a fiber-channel that receives (void) after ms milliseconds.\n  ;; Uses the existing timer-queue infrastructure.\n  (let ([ch (make-fiber-channel 1)]\n        [rt (current-fiber-runtime)])\n    (let* ([now (current-time 'time-utc)]\n           [deadline (add-duration now\n                       (make-time 'time-duration\n                                  (* (fxmod ms 1000) 1000000)\n                                  (fxquotient ms 1000)))])\n      ;; Spawn a tiny fiber that sleeps then sends\n      (fiber-spawn* (lambda ()\n        (fiber-sleep ms)\n        (fiber-channel-try-send ch (void)))))\n    ch))\n```\n\nUsage with `fiber-select`:\n```scheme\n(fiber-select\n  [work-ch result => (process result)]\n  [(fiber-timeout 5000) _ => (error 'timeout \"took too long\")])\n```\n\nBetter approach (no extra fiber): register directly on the timer queue, wake a channel when the deadline fires. Requires a small extension to the timer queue to fire arbitrary callbacks, not just wake fibers.\n\n**Effort**: ~1 hour (spawn approach), ~2 hours (timer-queue callback approach). **Tests**: timeout fires, timeout not needed (work completes first).\n\n---\n\n## Implementation Order\n\n| Priority | Gap | Effort | Dependencies |\n|----------|-----|--------|--------------|\n| 1 | cpu-count fix | 30 min | none |\n| 2 | Fiber cancellation | 3 hours | none |\n| 3 | Fiber-local storage | 2 hours | none |\n| 4 | Error propagation (fiber-join) | 2 hours | cancellation (for timeout variant) |\n| 5 | Fiber-channel select | 4 hours | none (but better with timeouts) |\n| 6 | Fiber-aware timeouts | 2 hours | select |\n| 7 | Structured concurrency | 4 hours | cancellation + error propagation |\n| 8 | Error propagation (fiber-link) | 3 hours | cancellation |\n\n**Total**: ~20 hours of implementation.\n\nGaps 1-4 are independent and can be done in any order. Gap 5 (select) unlocks gap 6 (timeouts). Gap 7 (structured concurrency) needs gaps 2 + 4.\n\n---\n\n## Existing Infrastructure to Reuse\n\n| Need | Source | Path |\n|------|--------|------|\n| Real CPU detection | actor scheduler | `lib/std/actor/scheduler.sls:135-146` |\n| Cancel tokens | task groups | `lib/std/task.sls:36-53` |\n| Supervision patterns | actor supervisor | `lib/std/actor/supervisor.sls` |\n| Spin-poll select | CSP select | `lib/std/csp/select.sls:166-174` |\n| Timer wheel | CSP select | `lib/std/csp/select.sls:299-410` |\n| Event abstraction | event system | `lib/std/event.sls:43-61` |\n| Error aggregation | task groups | `lib/std/task.sls:121-149` |\n| Hierarchical cleanup | custodians | `lib/std/misc/custodian.sls` |\n\n---\n\n## What's NOT in scope\n\n- **Work stealing**: The current run-queue is a single shared FIFO. A per-worker deque with stealing (like `actor/scheduler.sls`) would improve cache locality under heavy load, but the current design works well up to ~10K fibers. Optimize later if profiling shows contention.\n- **Fiber migration**: Moving a parked fiber from one worker to another. Engine continuations are not trivially portable across threads in Chez. Not needed for correctness, only for load balancing (which work-stealing would address).\n- **Async I/O integration**: Tying fiber parking to epoll/kqueue so that file/socket readiness wakes a fiber. This is a large project (essentially an event loop runtime) and orthogonal to the gaps above.\n"}
 {"text": "<!-- FILE: docs/pending.md -->\n# Pending: Making Jerboa Better\n\nLast updated: 2026-03-22.\n\n## Current State\n\n500+ modules, ~126K lines, 3,100+ tests, 13 fuzz harnesses. Full Gerbil API surface on stock Chez Scheme. Rust native backend (`libjerboa_native.so`) for crypto, compression, regex, databases, and OS integration. Legacy chez-* C FFI shims still available as fallbacks. A complete editor (jerboa-emacs) with TUI + Qt, Org-mode, LSP, Git.\n\n---\n\n## Language Completeness\n\n### chez-srfis \u2014 Standalone SRFI Repository\n\n35 SRFIs currently in `lib/std/srfi/`. Create `~/mine/chez-srfis` as a standalone repo (like `chez-r7rs`) and push toward 100+.\n\n**Priority SRFIs:**\n- SRFI-41 (streams) \u2014 standard API over lazy-seq\n- SRFI-64 (test framework)\n- SRFI-69 (hash tables) \u2014 older widely-used API\n- SRFI-152 (strings)\n- SRFI-170 (POSIX API) \u2014 complement `(std os *)`\n- SRFI-171 (transducers) \u2014 standard API over `(std transducer)`\n- SRFI-196 (ranges)\n\nJerboa keeps thin `(std srfi srfi-N)` re-export wrappers for namespace/conflict handling.\n\n### Integrate Remaining chez-* Libraries\n\nMost chez-* C FFI libraries have been superseded by the Rust native backend (see `docs/native-rust.md`). The remaining chez-* libraries that still need integration:\n\n| Library | Integration |\n|---------|------------|\n| **chez-qt** | Add to `CHEZ_EXT_LIBDIRS`, create `(std gui qt)` wrapper |\n| **chez-scintilla** | Already used by jerboa-emacs; expose via `(std gui scintilla)` |\n| **chez-r7rs** | Add to library path for `(scheme base)` etc. |\n\n`chez-ssh` is done \u2014 protocol logic split into `(std net ssh ...)` (10 modules, 3,132 lines), FFI stays in `(chez-ssh crypto)`.\n\n### Complete Rust Native Migration\n\nMove remaining modules from chez-* to Rust native:\n- **TLS**: `(std net ssl)` still uses chez-ssl / OpenSSL \u2014 need rustls-ffi integration\n- **LevelDB**: `(std db leveldb)` still uses chez-leveldb \u2014 need rusty-leveldb or sled\n- **Default imports**: Main modules (e.g., `(std db sqlite)`) still import from chez-* \u2014 need to rewire to `-native` modules\n\n### Web Framework\n\n`(std net httpd)`, `(std net router)`, `(std text json)`, `(std db sqlite)`, `(std db conpool)`, `(std net websocket)`, `(std net rate)`, `(std net security-headers)` all exist. Glue them into `(std web)`:\n\n- Route definitions with parameter extraction\n- JSON request/response helpers\n- Middleware stack\n- Database integration with connection pooling\n- Static file serving\n\n### Database Migrations\n\n`(std db migrate)` \u2014 numbered migration files, up/down, tracking table. Works with both SQLite and PostgreSQL (via Rust native or legacy chez-* backends).\n\n---\n\n## Build & Deployment\n\n### Single-Binary Automation\n\n`jerboa build --static` \u2014 compile all modules \u2192 link with Rust native backend \u2192 one ELF binary. The pieces exist (docs/single-binary.md, jerboa-native-rs/), just needs one-command orchestration.\n\n---\n\n## Tooling\n\n### DAP Debugger\n\nTime-travel debugging, replay, and inspector already exist. A DAP server would connect them to jemacs (which already has LSP client infrastructure as a template) and VS Code.\n\n### Profiler Visualization\n\n`(std misc profile)` provides programmatic profiling. Add Chrome trace format export or SVG flamegraph generation. Could integrate into jemacs as a profiler mode.\n\n### Race Detector\n\nInstrument mutex/hashtable operations, detect happens-before violations, report conflicting access stack traces. Would catch bugs in code using raw `fork-thread` outside structured concurrency.\n\n---\n\n## Claude-as-Developer Improvements\n\n### Better CLAUDE.md for Jerboa\n\nThe current CLAUDE.md is oriented toward Gerbil MCP tools. It should also have:\n- Jerboa-specific patterns and idioms\n- Common R6RS gotchas (definition context, phase separation)\n- Which backend to use (Rust native vs legacy chez-*) for what\n- How jerboa's module namespace maps to Gerbil's\n- Testing conventions\n\n### Example Corpus\n\nMore `examples/` showing real patterns Claude can reference when generating code:\n- Web API with auth and database\n- CLI tool with subcommands\n- Data processing pipeline\n- Actor system with supervision\n- FFI binding to a C library\n- Qt GUI application\n\n---\n\n## Wishlist\n\nThings that would be cool but aren't blocking anything:\n\n- **Windows graceful degradation** \u2014 security modules warn instead of crash on non-Linux\n- **WASM Chez** \u2014 run jerboa in the browser (experimental territory)\n- **jemacs inline eval** \u2014 evaluate expression in buffer, show result inline (REPL-driven development)\n- **jemacs package browser** \u2014 browse available libraries (Rust native + chez-*) from the editor\n"}
diff --git a/cpt_corpus_v3.jsonl b/cpt_corpus_v3.jsonl
index 77e1b82..7103c92 100644
--- a/cpt_corpus_v3.jsonl
+++ b/cpt_corpus_v3.jsonl
@@ -341,7 +341,7 @@
 {"text": "<!-- FILE: docs/single-binary.md -->\n# Building a Self-Contained Chez Scheme Binary: Tricks and Techniques\n\nThis document captures the techniques for building a single ELF binary that embeds Chez Scheme's runtime, boot files, compiled modules, and FFI bindings. The binary works from any directory with zero external dependencies (beyond libc and system libraries).\n\n---\n\n## Table of Contents\n\n1. [Architecture Overview](#1-architecture-overview)\n2. [Embedding Boot Files as C Byte Arrays](#2-embedding-boot-files-as-c-byte-arrays)\n3. [The Threading Bug: Separating Libraries from Program](#3-the-threading-bug-separating-libraries-from-program)\n4. [Loading the Program via memfd](#4-loading-the-program-via-memfd)\n5. [Custom C Main: Bypassing Chez's Argument Parsing](#5-custom-c-main-bypassing-chezs-argument-parsing)\n6. [The Jerboa Compilation Pipeline](#6-the-jerboa-compilation-pipeline)\n7. [Import Conflict Resolution (R6RS Constraints)](#7-import-conflict-resolution-r6rs-constraints)\n8. [Mutable Export Workaround (identifier-syntax)](#8-mutable-export-workaround-identifier-syntax)\n9. [Chez Lazy Library Invocation Workaround](#9-chez-lazy-library-invocation-workaround)\n10. [Post-Build Patching](#10-post-build-patching)\n11. [The Gambit-to-Chez Compatibility Layer](#11-the-gambit-to-chez-compatibility-layer)\n12. [FFI via C Shim (No Foreign Library Dependencies)](#12-ffi-via-c-shim-no-foreign-library-dependencies)\n13. [WPO Incompatibility with identifier-syntax](#13-wpo-incompatibility-with-identifier-syntax)\n14. [Boot File Dependency Ordering](#14-boot-file-dependency-ordering)\n15. [Build Pipeline Summary](#15-build-pipeline-summary)\n\n---\n\n## 1. Architecture Overview\n\nThe binary packs four layers into one ELF:\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  jsh-main.c (custom C entry point)       \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  petite.boot  \u2500\u2510                         \u2502\n\u2502  scheme.boot   \u251c\u2500 C byte arrays          \u2502\n\u2502  jsh.boot     \u2500\u2518  (Sregister_boot_file_bytes) \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  jsh_program.h  (compiled program .so    \u2502\n\u2502                   as C byte array,       \u2502\n\u2502                   loaded via memfd)       \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  ffi-shim.c  (POSIX C bindings)         \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  libkernel  (Chez Scheme runtime, linked)\u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\nThe key insight: everything that would normally be separate files (boot files, compiled .so modules, the program itself) is converted to C byte arrays at build time and compiled directly into the binary.\n\n---\n\n## 2. Embedding Boot Files as C Byte Arrays\n\n**Problem**: Chez Scheme normally locates boot files by searching paths relative to the executable name. If you rename or move the binary, it can't find them. The traditional workaround \u2014 appending boot files to the ELF with `cat` \u2014 still depends on the binary name matching the boot file name (e.g., `jsh` needs `jsh.boot`).\n\n**Solution**: Use `Sregister_boot_file_bytes()` to register boot file contents from memory. At build time, each `.boot` file is serialized into a C header:\n\n```scheme\n;; build-binary.ss \u2014 generates C headers from binary files\n(define (file->c-header input-path output-path array-name size-name)\n  (let* ((data (get-bytevector-all (open-file-input-port input-path)))\n         (size (bytevector-length data)))\n    (call-with-output-file output-path\n      (lambda (out)\n        (fprintf out \"static const unsigned char ~a[] = {~n\" array-name)\n        (let loop ((i 0))\n          (when (< i size)\n            (fprintf out \"0x~2,'0x\" (bytevector-u8-ref data i))\n            ...))\n        (fprintf out \"static const unsigned int ~a = ~a;~n\" size-name size)))))\n```\n\nThis produces headers like:\n```c\n// jsh_petite_boot.h \u2014 ~1.9 MB as C array\nstatic const unsigned char petite_boot_data[] = {\n  0x00,0x00,0x00,0x00,0x63,0x68,0x65,0x7a,...\n};\nstatic const unsigned int petite_boot_size = 1961464;\n```\n\nAt startup, the C main registers them:\n```c\nSregister_boot_file_bytes(\"petite\", (void*)petite_boot_data, petite_boot_size);\nSregister_boot_file_bytes(\"scheme\", (void*)scheme_boot_data, scheme_boot_size);\nSregister_boot_file_bytes(\"jsh\",    (void*)jsh_boot_data,    jsh_boot_size);\nSbuild_heap(NULL, NULL);  // NULL \u2014 no need to search the filesystem\n```\n\nThe boot files total ~5.5 MB (petite ~1.9 MB, scheme ~1.0 MB, jsh ~2.6 MB), all compiled into the binary's `.rodata` section.\n\n---\n\n## 3. The Threading Bug: Separating Libraries from Program\n\n**Problem**: When a Chez Scheme program is included in a boot file via `make-boot-file`, any threads created by `fork-thread` will deadlock. The child threads block forever on an internal GC futex. This is a Chez Scheme bug.\n\n**Discovery**: The shell uses threads for pipeline parallelism and background jobs. Including the program in the boot file caused every pipeline to hang.\n\n**Solution**: Split the boot file into **libraries-only** (no program code). The program is compiled separately and loaded at runtime:\n\n```scheme\n;; Boot file contains ONLY libraries \u2014 no program\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n  (append\n    jerboa-runtime-modules    ; types, MOP, error, hash, compiler...\n    compat-layer-modules      ; gambit shims, misc, sugar, pregexp...\n    jsh-modules))             ; all shell modules in dependency order\n\n;; Program is compiled separately\n(compile-program \"jsh.ss\")    ; produces jsh.so\n```\n\nThe program `.so` is then embedded as a C byte array and loaded via `Sscheme_script` at runtime (see next section).\n\n---\n\n## 4. Loading the Program via memfd\n\n**Problem**: `Sscheme_script()` takes a file path, but the program `.so` is embedded in the binary \u2014 there's no file on disk.\n\n**Solution**: Use Linux's `memfd_create()` to create an anonymous in-memory file descriptor, write the embedded program data to it, and pass `/proc/self/fd/N` as the path:\n\n```c\n// Create anonymous memory-backed fd\nint fd = memfd_create(\"jsh-program\", MFD_CLOEXEC);\nwrite(fd, jsh_program_data, jsh_program_size);\n\n// Chez can load it via /proc/self/fd/N\nchar prog_path[64];\nsnprintf(prog_path, sizeof(prog_path), \"/proc/self/fd/%d\", fd);\n\n// Sbuild_heap loads libraries from boot files\nSbuild_heap(NULL, NULL);\n\n// Sscheme_script loads+runs the program (preserves threading!)\nint status = Sscheme_script(prog_path, 1, script_args);\n```\n\n**Why `Sscheme_script` instead of `Sscheme_start`**: `Sscheme_start` is for programs baked into boot files (which deadlock threads). `Sscheme_script` loads and runs a `.so` file after the heap is built, preserving full threading support.\n\n---\n\n## 5. Custom C Main: Bypassing Chez's Argument Parsing\n\n**Problem**: Chez Scheme's default `main()` interprets command-line flags. The shell's `-c` flag (run command string) is swallowed by Chez as `--compact` (heap compaction). Similar conflicts with `-e`, `-q`, etc.\n\n**Solution**: Write a custom `jsh-main.c` that saves all arguments in numbered environment variables before Chez sees them:\n\n```c\n// C main saves args in env vars\nsetenv(\"JSH_ARGC\", \"3\", 1);\nsetenv(\"JSH_ARG0\", \"-c\", 1);\nsetenv(\"JSH_ARG1\", \"echo hello\", 1);\nsetenv(\"JSH_ARG2\", \"world\", 1);\n\n// Chez is initialized with NO user args\nSscheme_init(NULL);\nSbuild_heap(NULL, NULL);\n```\n\nThe Scheme entry point reads them back:\n```scheme\n(define (get-real-args)\n  (let ((argc-str (getenv \"JSH_ARGC\")))\n    (if argc-str\n      ;; Binary mode: read from env vars\n      (let ((argc (string->number argc-str)))\n        (let loop ((i 0) (acc '()))\n          (if (>= i argc) (reverse acc)\n            (loop (+ i 1) (cons (getenv (format \"JSH_ARG~a\" i)) acc)))))\n      ;; Interpreted mode: use (command-line)\n      (cdr (command-line)))))\n```\n\nLink with the custom main instead of Chez's `main.o`:\n```\ngcc -rdynamic -o jsh jsh-main.o ffi-shim.o -lkernel -llz4 -lz -lm -ldl -lpthread -luuid -lncurses\n```\n\n---\n\n## 6. The Jerboa Compilation Pipeline\n\n**Problem**: Source files are written in Gerbil Scheme (`.ss`) which uses syntax, macros, and module system conventions incompatible with Chez's R6RS. The files must be preserved as `.ss` and only converted at build time.\n\n**Solution**: The Jerboa compiler (`gerbil-compile-to-library`) transforms each `.ss` file into an R6RS `(library ...)` form. An import map translates Gerbil module references to Chez library names:\n\n```scheme\n(define jsh-import-map\n  '((:std/sugar        . (compat sugar))     ; Gerbil sugar -> our compat lib\n    (:std/format       . (compat format))\n    (:std/pregexp      . (compat pregexp))\n    (:std/iter         . #f)                  ; stripped \u2014 Jerboa handles natively\n    (:std/foreign      . #f)                  ; stripped \u2014 not needed on Chez\n    (:gerbil/core      . #f)                  ; stripped\n    (\"./pregexp-compat\" . (jsh pregexp-compat)) ; relative import\n    ...))\n```\n\nBase imports provide the Gambit/Gerbil runtime compatibility layer that every module needs:\n```scheme\n(define jsh-base-imports\n  '((except (chezscheme) void box box? unbox set-box! ...)  ; exclude conflicts\n    (compat types)          ; Gerbil type system\n    (runtime util)          ; Gerbil runtime utilities\n    (runtime mop)           ; Meta-object protocol\n    (runtime error)         ; Gerbil error types\n    (runtime hash)          ; Hash tables\n    (compat gambit)         ; Gambit compatibility shims\n    (compat misc)))         ; String/list/path utilities\n```\n\nModules must be compiled in dependency order (7 tiers, from `ast` up to `main`).\n\n---\n\n## 7. Import Conflict Resolution (R6RS Constraints)\n\n**Problem**: R6RS forbids local definitions that shadow imported bindings. After Jerboa compilation, many modules define names that conflict with their imports (e.g., a module defines `find` which conflicts with `(compat misc)`'s `find`).\n\n**Solution**: A post-compilation pass (`fix-import-conflicts`) introspects each library's imports using `library-exports`, identifies conflicts with local definitions, and automatically adds `(except ...)` clauses:\n\n```scheme\n;; Before: bare import conflicts with local (define find ...)\n(import (compat misc))\n\n;; After: automatically patched\n(import (except (compat misc) find string-split))\n```\n\nThe algorithm also handles **import-vs-import** conflicts where two imported libraries export the same name \u2014 the later import gets an `except` clause for names already provided by earlier imports.\n\nAs a fallback when `library-exports` fails (library not yet loaded), the system reads exports directly from `.sls` files by parsing the `(library <name> (export ...) ...)` header.\n\n---\n\n## 8. Mutable Export Workaround (identifier-syntax)\n\n**Problem**: R6RS forbids `set!` on exported variables. Gerbil code frequently uses patterns like:\n\n```scheme\n(export *my-parameter*)\n(define *my-parameter* #f)\n...\n(set! *my-parameter* new-value)\n```\n\nThis is legal in Gerbil but illegal in R6RS.\n\n**Solution**: The build system scans for `(set! name ...)` where `name` is in the export list, then rewrites the definition to use `identifier-syntax` \u2014 a Chez macro that provides transparent get/set:\n\n```scheme\n;; Original (illegal in R6RS):\n(define *foo* initial-value)\n(set! *foo* new-value)        ; ERROR: exported variable\n\n;; Rewritten (transparent to all code):\n(define *foo*-cell (vector initial-value))\n(define-syntax *foo*\n  (identifier-syntax\n    (id (vector-ref *foo*-cell 0))\n    ((set! id v) (vector-set! *foo*-cell 0 v))))\n\n;; All existing (set! *foo* ...) and *foo* references work unchanged\n```\n\nThis transformation is invisible to all consuming code \u2014 reads and writes work exactly as before.\n\n---\n\n## 9. Chez Lazy Library Invocation Workaround\n\n**Problem**: Chez Scheme lazily invokes library bodies. A library's top-level expressions only run when a *runtime* export (not a macro) is first referenced. The `builtins.ss` module has side-effecting top-level expressions (`defbuiltin` calls) that register builtin commands into a global registry. If no code explicitly references a builtins export, these registrations never happen.\n\n**Solution**: Inject a dummy reference in `main.sls` that forces Chez to invoke `(jsh builtins)`:\n\n```scheme\n;; Patched into main.sls after Jerboa compilation:\n(define _force-builtins special-builtin?)  ; references a builtins export\n```\n\nThe entry point `jsh.ss` also forces it:\n```scheme\n(let () special-builtin? (void))  ; force (jsh builtins) invocation\n```\n\n---\n\n## 10. Post-Build Patching\n\nAfter Jerboa compilation, several patterns in the generated `.sls` files need fixing. These are applied as string-level patches on the generated code:\n\n### 10a. Keyword Dispatch in defclass Constructors\n\n**Bug**: Jerboa's `defclass` constructor translation incorrectly appends keyword argument *values* to the positional argument list. The generated `init!` method is a `case-lambda` dispatching on positional arity, so extra values cause the wrong clause to match.\n\n**Patch**:\n```scheme\n;; Before: keyword values added to positional args\n(lp (cddr rest) (cons (cadr rest) acc))\n\n;; After: skip keyword pairs entirely\n(lp (cddr rest) acc)\n```\n\nCallers that used keyword syntax are also patched to positional:\n```scheme\n;; Before:\n(make-shell-environment 'parent: env 'name: name)\n\n;; After:\n(make-shell-environment env name)\n```\n\n### 10b. Exception Messages\n\n**Bug**: Chez's `condition-message` returns raw format templates (e.g., `\"variable ~:s is not bound\"`) with `~:s` placeholders, unlike Gambit which returns formatted strings.\n\n**Patch**: Replace `exception-message` to use `display-condition` which formats the message with its irritants, then strip the `\"Exception: \"` or `\"Exception in <who>: \"` prefix.\n\n### 10c. make-mutex Argument Type\n\n**Bug**: Gambit's `make-mutex` accepts strings; Chez requires a symbol or `#f`.\n\n**Patch**: `(make-mutex \"pipeline-fd\")` -> `(make-mutex 'pipeline-fd)`\n\n### 10d. Recursive void Definition\n\n**Bug**: Gerbil's `void` is variadic (accepts any args, returns void). Jerboa translates `(define (void) (void))` which creates a 0-arg function that infinitely recurses.\n\n**Patch**: `(define (void) (void))` -> `(define (void . _) (if #f #f))`\n\n---\n\n## 11. The Gambit-to-Chez Compatibility Layer\n\nThe `src/compat/` directory provides ~900 lines of shims translating Gambit APIs to Chez equivalents. Key modules:\n\n### gambit.sls (~645 lines)\n- **u8vector/bytevector**: Gambit's `u8vector` API mapped to Chez bytevectors\n- **Threading**: `spawn`, `thread-join!`, `mutex-lock!`/`mutex-unlock!` -> Chez `fork-thread`, `mutex-acquire`/`mutex-release`\n- **Processes**: `open-process`, `process-status`, `process-pid` -> Chez `process` with pipe-based communication\n- **File system**: `file-info`, `file-type`, `directory-files` -> Chez equivalents with Gambit-compatible return types\n- **I/O**: `/dev/fd/N` support (Gambit feature), keyword argument handling for `open-input-file`/`open-output-file`\n- **tty-mode-set!**: Terminal raw mode via FFI\n\n### sugar.sls (~184 lines)\n- `def`, `def*`, `defrule` (Gerbil syntax macros)\n- `try`/`catch`/`finally`, `with-catch`, `unwind-protect`\n- `while`, `until` loops\n- `defstruct` record type definitions\n- Custom `error` function (Gerbil-style, no `who` parameter)\n\n### misc.sls (~200 lines)\n- String: `string-prefix?`, `string-suffix?`, `string-split`, `string-join`, `string-trim`, `string-contains`, `string-index`\n- List: `flatten`, `unique`, `every`, `any`, `iota`, `last`\n- Path: `path-expand`, `path-directory`, `path-extension`\n- Hash: `hash-ref` with default, `hash-copy`, `hash->list`\n\n### Runtime Shims in jsh.ss\n\nThe entry point installs additional Gambit primitives into the interaction environment for Gerbil eval:\n\n```scheme\n;; f64vector -> flvector\n(define (make-f64vector n . rest) (make-flvector n ...))\n\n;; Threading: vector-based thread objects with mutex+condition\n(define (make-thread thunk) (vector thunk (make-mutex) (make-condition) #f #f))\n(define (thread-start! thr) (fork-thread ...))\n(define (thread-join! thr) (mutex-acquire ...) (condition-wait ...))\n\n;; Gambit ##process-statistics -> flvector with cpu/real/gc times\n(define (##process-statistics) ...)\n\n;; SMP primitives -> no-ops\n(define (##set-parallelism-level! n) (void))\n```\n\n---\n\n## 12. FFI via C Shim (No Foreign Library Dependencies)\n\n**Problem**: The shell needs low-level POSIX operations (fork/exec, signal handling, terminal control, job control) that Chez doesn't expose directly.\n\n**Solution**: A single C file (`ffi-shim.c`, ~480 lines) provides all POSIX bindings. It's compiled as a shared library for interpreted mode and as an object file linked directly into the binary:\n\n- **Signal handling**: Flag-based async-signal-safe mechanism (`_signal_flags[]` + `sigaction`)\n- **Fork/exec**: `ffi_fork_exec()` handles fork, signal reset, fd cleanup, process groups, and execve in C (no Scheme in the child process)\n- **Argument passing**: SOH (0x01) delimited packed strings for argv/envp \u2014 avoids marshaling string arrays across FFI\n- **Pipe/fd management**: Cached pipe fds, bulk fd close via `/proc/self/fd`\n- **Terminal**: Raw mode, termios save/restore, window size via `ioctl(TIOCGWINSZ)`\n- **Read buffer**: 1 MB static buffer for `ffi_do_read_all()` \u2014 avoids Scheme-side allocation for command substitution\n\nAll functions use standard POSIX libc \u2014 zero external library dependencies for the FFI layer.\n\n---\n\n## 13. WPO Incompatibility with identifier-syntax\n\n**Problem**: Chez's Whole-Program Optimization (WPO) analyzes all code and eliminates dead definitions. The `identifier-syntax` macros generated for mutable exports (see section 8) appear to WPO as unused macro-only definitions \u2014 it eliminates the underlying vector-cell storage, breaking everything.\n\n**Solution**: Skip WPO entirely. Use the plain `jsh.so` from `compile-program` without running `compile-whole-program`:\n\n```scheme\n;; DO NOT use compile-whole-program \u2014 it eliminates identifier-syntax cells\n(compile-program \"jsh.ss\")    ; produces jsh.so\n(system \"cp jsh.so jsh-all.so\")  ; use directly, no WPO pass\n```\n\nThe binary size penalty is minimal (~10%) and avoids subtle runtime crashes.\n\n---\n\n## 14. Boot File Dependency Ordering\n\nThe `make-boot-file` call must list `.so` files in strict dependency order. A module can only reference definitions from modules listed before it. Example order:\n\n```\n1. Jerboa runtime (9 modules):\n   types -> gambit-compat -> util -> table -> c3 -> mop -> error -> hash\n   -> syntax -> eval -> reader -> compile -> jerboa\n\n2. Compat layer (9 modules):\n   gambit -> misc -> sort -> sugar -> format -> pregexp\n   -> signal -> signal-handler -> fdio\n\n3. JSH modules (30 modules, 7 tiers):\n   Tier 1: ffi, pregexp-compat, stage, static-compat\n   Tier 2: ast, registry, macros, util\n   Tier 3: environment, lexer, arithmetic, glob, fuzzy, history\n   Tier 4: parser, functions, signals, expander\n   Tier 5: redirect, control, jobs, builtins\n   Tier 6: pipeline, executor, completion, prompt\n   Tier 7: lineedit, fzf, script, startup, main\n```\n\nGetting this order wrong produces cryptic \"unbound variable\" errors at runtime.\n\n---\n\n## 15. Build Pipeline Summary\n\n```\nStep 1: gcc -> libjsh-ffi.so          (C FFI shim, shared lib for interpreted mode)\nStep 2: Jerboa -> src/jsh/*.sls       (30 .ss files -> 30 .sls R6RS libraries)\n        + post-build patches           (import conflicts, mutable exports, Chez compat)\nStep 3: Chez compile-program          (jsh.ss -> jsh.so, compiles all imported libs)\nStep 4: make-boot-file                (48 .so files -> jsh.boot, libs-only)\nStep 5: file->c-header x 4            (petite.boot, scheme.boot, jsh.boot, jsh.so -> .h)\nStep 6: gcc -> jsh-main.o             (custom main with embedded boot+program arrays)\n        gcc -> ffi-shim.o             (POSIX bindings)\nStep 7: gcc -> jsh                    (link with -lkernel -llz4 -lz -lm -ldl -lpthread -luuid -lncurses)\n\nResult: ./jsh  (~6.5 MB, fully self-contained ELF)\n```\n\n### What's Inside the Binary\n\n| Component | Size | Purpose |\n|-----------|------|---------|\n| petite.boot | ~1.9 MB | Chez base runtime (I/O, syntax, eval) |\n| scheme.boot | ~1.0 MB | Full Chez compiler |\n| jsh.boot | ~2.6 MB | Jerboa runtime + compat + 30 shell modules |\n| jsh program | ~9 KB | Entry point + Gerbil eval handler |\n| ffi-shim | ~15 KB | POSIX C bindings |\n| libkernel | ~1.0 MB | Chez Scheme runtime (GC, threads, FFI) |\n\nThe binary runs from any directory, under any name, with no external Chez/Gerbil/Gambit installation required.\n"}
 {"text": "<!-- FILE: docs/pattern-matching.md -->\n# Pattern Matching 2.0 \u2014 `(std match2)`\n\nThe `(std match2)` library extends Chez Scheme with a powerful pattern matching system. It builds on the ideas behind `(std match)` and adds three major capabilities: **sealed hierarchies** (closed sets of variants that enable exhaustiveness checking), **active patterns** (user-defined extractors that run arbitrary code during matching), and **view patterns** (computed projections bound as match variables). Together these make it practical to write exhaustive, structurally typed dispatch code in Chez Scheme.\n\n## Table of Contents\n\n1. [Quick Start](#quick-start)\n2. [Core API](#core-api)\n3. [Pattern Syntax Reference](#pattern-syntax-reference)\n4. [Sealed Hierarchies](#sealed-hierarchies)\n5. [Active Patterns](#active-patterns)\n6. [Guards](#guards)\n7. [Complete Examples](#complete-examples)\n8. [Exhaustiveness Checking with `match/strict`](#exhaustiveness-checking-with-matchstrict)\n9. [Comparison with Standard Match](#comparison-with-standard-match)\n\n---\n\n## Quick Start\n\n```scheme\n(import (chezscheme) (std match2))\n\n;; Basic structural matching\n(match '(1 2 3)\n  [(list a b c) (+ a b c)])   ; => 6\n\n;; Predicate patterns\n(match 42\n  [(? string?) 'string]\n  [(? number?) 'number])      ; => 'number\n\n;; Struct patterns (after registering the type)\n(define-record-type (point make-point point?)\n  (fields (immutable x point-x) (immutable y point-y)))\n\n(define-match-type point point? point-x point-y)\n\n(match (make-point 3 4)\n  [(point x y) (sqrt (+ (* x x) (* y y)))])  ; => 5.0\n```\n\n---\n\n## Core API\n\n### `match`\n\n```scheme\n(match expr clause ...)\n```\n\nEvaluates `expr` and tests each `clause` in order until one matches. A clause has the form `(pattern body ...)` or `(pattern (where guard) body ...)`. The value of the first matching clause's body is returned. If no clause matches, an error is raised.\n\n```scheme\n(match value\n  [pattern1 result1]\n  [pattern2 result2]\n  [_ default])\n```\n\n### `define-sealed-hierarchy`\n\n```scheme\n(define-sealed-hierarchy hier-name\n  (variant-name pred-fn accessor ...)\n  ...)\n```\n\nDeclares a closed set of variant types under a single hierarchy name. Each variant is registered for structural matching and the group is registered as a sealed hierarchy for `match/strict` coverage checking.\n\n- `hier-name` \u2014 symbol naming the hierarchy (used with `match/strict`)\n- `variant-name` \u2014 symbol used as the pattern head (e.g., `(variant-name field ...)`)\n- `pred-fn` \u2014 predicate procedure for this variant\n- `accessor ...` \u2014 field accessor procedures in order\n\n```scheme\n(define-sealed-hierarchy expr\n  (lit-expr lit-expr? lit-value)\n  (add-expr add-expr? add-left add-right)\n  (var-expr var-expr? var-name))\n```\n\n### `sealed-hierarchy?`\n\n```scheme\n(sealed-hierarchy? name) => boolean\n```\n\nReturns `#t` if `name` (a symbol) has been registered as a sealed hierarchy.\n\n```scheme\n(sealed-hierarchy? 'expr)    ; => #t\n(sealed-hierarchy? 'unknown) ; => #f\n```\n\n### `sealed-hierarchy-members`\n\n```scheme\n(sealed-hierarchy-members name) => list\n```\n\nReturns the member list of a sealed hierarchy. Each element is a list `(variant-name pred-fn accessor ...)`. Returns `'()` if the hierarchy is not registered.\n\n```scheme\n(sealed-hierarchy-members 'expr)\n; => ((lit-expr #<procedure lit-expr?> #<procedure lit-value>)\n;     (add-expr #<procedure add-expr?> ...)\n;     ...)\n```\n\n### `register-struct-type!`\n\n```scheme\n(register-struct-type! name pred accessor ...)\n```\n\nRegisters a struct type for structural matching by name. After registration, `(name field1 field2 ...)` can be used as a pattern in `match`. This is the runtime procedure that `define-match-type` and `define-sealed-hierarchy` call internally.\n\n```scheme\n(define-record-type (point make-point point?)\n  (fields (immutable x point-x) (immutable y point-y)))\n\n(register-struct-type! 'point point? point-x point-y)\n```\n\n### `define-match-type`\n\n```scheme\n(define-match-type type-name pred-fn accessor ...)\n```\n\nConvenience macro that calls `register-struct-type!`. Use this to make any record type usable as a pattern head.\n\n```scheme\n(define-match-type point point? point-x point-y)\n\n;; Now usable in patterns:\n(match (make-point 1 2)\n  [(point x y) (list x y)])  ; => '(1 2)\n```\n\n### `match/strict`\n\n```scheme\n(match/strict sealed-type expr clause ...)\n(match/strict expr clause ...)\n```\n\nLike `match`, but signals intent that all variants of a sealed hierarchy are handled. The `sealed-type` argument names the hierarchy being matched; it is available for documentation and tooling purposes. If no clause matches at runtime, an error is raised (same as `match`).\n\nNote: Full static exhaustiveness checking is not possible at Chez macro-expansion time under R6RS phasing. `match/strict` is primarily a documentation and convention signal \u2014 it pairs naturally with `define-sealed-hierarchy` to communicate \"this match is supposed to cover all cases.\"\n\n```scheme\n(match/strict shape s\n  [(circle r)   (* pi r r)]\n  [(rect   w h) (* w h)])\n```\n\n### `define-active-pattern`\n\n```scheme\n(define-active-pattern (name input) body ...)\n```\n\nDefines a named extractor function for use in `match` patterns. The body receives the value being matched as `input` and should return:\n\n- `#f` \u2014 pattern does not match\n- `#t` \u2014 pattern matches, no extracted values\n- A list `(v ...)` \u2014 pattern matches, values are bound to sub-patterns\n- A vector `#(v ...)` \u2014 same as list form\n\nActive patterns integrate seamlessly into `match` syntax: `(name sub-pat ...)` is valid wherever any other structural pattern is valid.\n\n```scheme\n(define-active-pattern (even? n)\n  (and (integer? n) (even? n)))\n\n(define-active-pattern (split-at-comma s)\n  (if (string? s)\n    (let ([i (string-contains s \",\")])\n      (if i\n        (list (substring s 0 i)\n              (substring s (+ i 1) (string-length s)))\n        #f))\n    #f))\n```\n\n### `active-pattern?`\n\n```scheme\n(active-pattern? name) => boolean\n```\n\nReturns `#t` if `name` (a symbol) has been registered via `define-active-pattern`.\n\n### `active-pattern-proc`\n\n```scheme\n(active-pattern-proc name) => procedure | #f\n```\n\nReturns the extractor procedure for an active pattern, or `#f` if not registered. Useful for introspection or testing active patterns directly.\n\n```scheme\n((active-pattern-proc 'split-at-comma) \"a,b\")  ; => (\"a\" \"b\")\n((active-pattern-proc 'split-at-comma) \"nope\")  ; => #f\n```\n\n---\n\n## Pattern Syntax Reference\n\n### Wildcard\n\n```scheme\n_\n```\n\nMatches any value without binding.\n\n```scheme\n(match x [_ 'anything])\n```\n\n### Variables\n\n```scheme\nname\n```\n\nAny identifier (other than `_` and reserved pattern keywords) binds the matched value.\n\n```scheme\n(match 42 [n (* n 2)])  ; => 84\n```\n\n### Literals\n\n| Pattern | Matches |\n|---------|---------|\n| `42` | Number equal to 42 |\n| `\"hello\"` | String equal to `\"hello\"` |\n| `#t` / `#f` | Boolean |\n| `'foo` | Symbol `foo` |\n| `'(1 2)` | Any datum equal under `equal?` |\n\n```scheme\n(match x\n  [0    \"zero\"]\n  [1    \"one\"]\n  [\"hi\" \"greeting\"]\n  ['ok  \"ok symbol\"])\n```\n\n### Predicate: `(? pred)`\n\n```scheme\n(? predicate)\n```\n\nMatches if `(predicate value)` is truthy. Does not bind the value.\n\n```scheme\n(match x\n  [(? number?) 'numeric]\n  [(? string?) 'textual])\n```\n\n### Predicate with Binding: `(? pred -> var)`\n\n```scheme\n(? predicate -> variable)\n```\n\nApplies `predicate` to the value. If the result is truthy, binds it to `variable` and succeeds. This is useful when the predicate returns a transformed value (e.g., `string->number`).\n\n```scheme\n(match \"42\"\n  [(? string->number -> n) (* n 10)]  ; => 420\n  [_ 'not-a-number])\n```\n\n### View Pattern: `(=> proc var)`\n\n```scheme\n(=> procedure variable)\n```\n\nApplies `procedure` to the value unconditionally and binds the result to `variable`. Always succeeds. Use this for projections or computed views of the input.\n\n```scheme\n(match \"hello\"\n  [(=> string-length len) len])  ; => 5\n\n(match 10\n  [(=> (lambda (x) (* x x)) sq) sq])  ; => 100\n```\n\n### Conjunction: `(and pat ...)`\n\n```scheme\n(and pattern ...)\n```\n\nMatches if all sub-patterns match. Bindings from all sub-patterns are in scope in the body. `(and)` with no sub-patterns always succeeds.\n\n```scheme\n(match x\n  [(and (? number?) (? positive?)) 'positive-number]\n  [(and (? number?) n)             (list 'nonpositive n)])\n```\n\n### Disjunction: `(or pat ...)`\n\n```scheme\n(or pattern ...)\n```\n\nMatches if any sub-pattern matches. Because different branches may bind different variables, bindings from `or` sub-patterns are not visible in the body \u2014 use `or` only for pure dispatch without variable capture. `(or)` with no sub-patterns never matches.\n\n```scheme\n(match x\n  [(or 1 2 3) 'small]\n  [_          'large])\n```\n\n### Negation: `(not pat)`\n\n```scheme\n(not pattern)\n```\n\nMatches if the sub-pattern does not match. No bindings are captured.\n\n```scheme\n(match x\n  [(not (? string?)) 'not-a-string]\n  [_                 'string])\n```\n\n### Pair: `(cons p1 p2)`\n\n```scheme\n(cons car-pattern cdr-pattern)\n```\n\nMatches a pair. Binds the car to `car-pattern` and the cdr to `cdr-pattern`.\n\n```scheme\n(match '(1 2 3)\n  [(cons h t) (list 'head h 'tail t)])  ; => '(head 1 tail (2 3))\n```\n\n### Exact List: `(list pat ...)`\n\n```scheme\n(list pattern ...)\n```\n\nMatches a proper list of exactly the given length. Each sub-pattern matches the corresponding element.\n\n```scheme\n(match '(x y z)\n  [(list a b c) (list c b a)])  ; => '(z y x)\n\n(match '()\n  [(list) 'empty])\n```\n\n### Improper / Prefix List: `(list* pat ... rest)`\n\n```scheme\n(list* pattern ... rest-pattern)\n```\n\nMatches a list with at least n elements (where n is the number of leading patterns). The leading patterns match elements 0 through n-1; `rest-pattern` matches the tail starting at element n.\n\n```scheme\n(match '(1 2 3 4 5)\n  [(list* a b rest) (list a b rest)])  ; => '(1 2 (3 4 5))\n```\n\n### Vector: `(vector pat ...)`\n\n```scheme\n(vector pattern ...)\n```\n\nMatches a vector of exactly the given length.\n\n```scheme\n(match (vector 10 20 30)\n  [(vector a b c) (+ a b c)])  ; => 60\n```\n\n### Box: `(box pat)`\n\n```scheme\n(box pattern)\n```\n\nMatches a Chez Scheme `box` and applies the sub-pattern to the unboxed value.\n\n```scheme\n(match (box 99)\n  [(box n) n])  ; => 99\n```\n\n### Struct / Named Pattern: `(TypeName pat ...)`\n\n```scheme\n(TypeName field-pattern ...)\n```\n\nMatches a struct value whose type was registered via `register-struct-type!`, `define-match-type`, or `define-sealed-hierarchy`. The sub-patterns are matched against the field values in registration order.\n\nWorks identically for both struct types and active patterns \u2014 the runtime dispatcher tries active patterns first, then struct types.\n\n```scheme\n(match (make-point 3 4)\n  [(point x y) (+ x y)])  ; => 7\n```\n\n### Pattern Guards: `(where expr)`\n\nA guard condition can appear as the first form after the pattern in a clause:\n\n```scheme\n(pattern (where guard-expression) body ...)\n```\n\nThe guard is evaluated with all pattern bindings in scope. If it returns `#f`, the clause is skipped and matching continues with the next clause.\n\n```scheme\n(match '(3 4)\n  [(list a b) (where (= a b)) 'equal]\n  [(list a b)                 'different])  ; => 'different\n\n(match 42\n  [n (where (> n 100)) 'big]\n  [n                   (list 'small n)])    ; => '(small 42)\n```\n\n---\n\n## Sealed Hierarchies\n\nA sealed hierarchy declares that a set of variants is closed \u2014 there are no other variants. This is the Jerboa equivalent of a sum type or tagged union.\n\n```scheme\n;; 1. Define the record types\n(define-record-type (circle make-circle circle?)\n  (fields (immutable radius circle-radius)))\n\n(define-record-type (rect make-rect rect?)\n  (fields (immutable w rect-w) (immutable h rect-h)))\n\n(define-record-type (triangle make-triangle triangle?)\n  (fields (immutable base triangle-base) (immutable height triangle-height)))\n\n;; 2. Declare the sealed hierarchy\n(define-sealed-hierarchy shape\n  (circle   circle?   circle-radius)\n  (rect     rect?     rect-w rect-h)\n  (triangle triangle? triangle-base triangle-height))\n\n;; 3. Match against variants\n(define (area s)\n  (match/strict shape s\n    [(circle   r)   (* 3.14159 r r)]\n    [(rect     w h) (* w h)]\n    [(triangle b h) (/ (* b h) 2.0)]))\n\n(area (make-circle 5))         ; => 78.53975\n(area (make-rect 3 4))         ; => 12\n(area (make-triangle 6 4))     ; => 12.0\n```\n\n`define-sealed-hierarchy` automatically calls `register-struct-type!` for each variant, so the variants are immediately usable in ordinary `match` as well.\n\nIntrospection:\n\n```scheme\n(sealed-hierarchy? 'shape)        ; => #t\n(length (sealed-hierarchy-members 'shape))  ; => 3\n```\n\n---\n\n## Active Patterns\n\nActive patterns let you define custom extractors \u2014 matching logic and value transformation in one place. They integrate into `match` syntax as named pattern heads.\n\n### Boolean Active Patterns\n\nReturn `#t` or `#f` for predicate-only tests:\n\n```scheme\n(define-active-pattern (even-int? n)\n  (and (integer? n) (even? n)))\n\n(match 4\n  [(even-int?) 'even]\n  [_           'odd])   ; => 'even\n```\n\n### Extracting Active Patterns\n\nReturn a list to provide values to sub-patterns:\n\n```scheme\n(define-active-pattern (double n)\n  (list (* n 2)))\n\n(match 5\n  [(double d) d])   ; => 10\n```\n\n### Multi-Value Extraction\n\n```scheme\n(define-active-pattern (parse-kv s)\n  ;; Splits \"key=value\" into two strings, or fails\n  (and (string? s)\n       (let loop ([i 0])\n         (cond\n           [(= i (string-length s)) #f]\n           [(char=? (string-ref s i) #\\=)\n            (list (substring s 0 i)\n                  (substring s (+ i 1) (string-length s)))]\n           [else (loop (+ i 1))]))))\n\n(match \"name=Alice\"\n  [(parse-kv k v) (list 'key k 'val v)]\n  [_              'no-equals])\n; => '(key \"name\" val \"Alice\")\n\n(match \"no-equals\"\n  [(parse-kv k v) (list k v)]\n  [_              'no-equals])\n; => 'no-equals\n```\n\n### Nesting Active Patterns\n\nActive patterns can be nested with other patterns:\n\n```scheme\n(define-active-pattern (abs-val n)\n  (list (if (negative? n) (- n) n)))\n\n(match -42\n  [(abs-val (? (lambda (x) (> x 10)))) 'large-magnitude]\n  [(abs-val a)                          (list 'magnitude a)])\n; => 'large-magnitude\n```\n\n---\n\n## Complete Examples\n\n### AST Processing with Sealed Hierarchy\n\n```scheme\n(import (chezscheme) (std match2))\n\n;; Define an expression AST\n(define-record-type (num-node  make-num  num?)\n  (fields (immutable value num-value)))\n(define-record-type (add-node  make-add  add?)\n  (fields (immutable left add-left) (immutable right add-right)))\n(define-record-type (mul-node  make-mul  mul?)\n  (fields (immutable left mul-left) (immutable right mul-right)))\n(define-record-type (var-node  make-var  var?)\n  (fields (immutable name var-name)))\n\n(define-sealed-hierarchy expr\n  (num-node num?  num-value)\n  (add-node add?  add-left add-right)\n  (mul-node mul?  mul-left mul-right)\n  (var-node var?  var-name))\n\n;; Evaluator\n(define (eval-expr node env)\n  (match/strict expr node\n    [(num-node v)   v]\n    [(add-node l r) (+ (eval-expr l env) (eval-expr r env))]\n    [(mul-node l r) (* (eval-expr l env) (eval-expr r env))]\n    [(var-node n)   (cdr (assq n env))]))\n\n(define my-env '((x . 3) (y . 4)))\n\n(eval-expr\n  (make-add (make-mul (make-var 'x) (make-var 'x))\n            (make-mul (make-var 'y) (make-var 'y)))\n  my-env)\n; => 25  (3*3 + 4*4)\n\n;; Pretty printer\n(define (pp-expr node)\n  (match/strict expr node\n    [(num-node v)   (number->string v)]\n    [(var-node n)   (symbol->string n)]\n    [(add-node l r) (string-append \"(\" (pp-expr l) \" + \" (pp-expr r) \")\")]\n    [(mul-node l r) (string-append \"(\" (pp-expr l) \" * \" (pp-expr r) \")\")]))\n\n(pp-expr (make-add (make-var 'x) (make-num 1)))\n; => \"(x + 1)\"\n```\n\n### JSON Value Matching\n\n```scheme\n(import (chezscheme) (std match2))\n\n;; JSON values are represented as: number, string, boolean, 'null,\n;; (vector ...) for arrays, and alists for objects.\n\n(define (json-type v)\n  (match v\n    [(? number?)  'number]\n    [(? string?)  'string]\n    [#t           'true]\n    [#f           'false]\n    ['null        'null]\n    [(? vector?)  'array]\n    [(? pair?)    'object]\n    [_            'unknown]))\n\n(define (json-get obj key)\n  (match obj\n    [(? pair?) (let ([entry (assoc key obj)])\n                 (if entry (cdr entry) 'null))]\n    [_         'null]))\n\n(define (json-summarize v)\n  (match v\n    [(? number?)              (format \"~a\" v)]\n    [(? string?)              (format \"\\\"~a\\\"\" v)]\n    [#t                       \"true\"]\n    [#f                       \"false\"]\n    ['null                    \"null\"]\n    [(? vector?)\n     (format \"[~a items]\" (vector-length v))]\n    [(list* (cons _ _) rest)\n     (format \"{~a keys}\" (+ 1 (length rest)))]\n    [_                        \"?\"]))\n\n(json-type 42)            ; => 'number\n(json-type \"hello\")       ; => 'string\n(json-type #t)            ; => 'true\n(json-type '((x . 1)))   ; => 'object\n(json-summarize '((name . \"Alice\") (age . 30)))  ; => \"{2 keys}\"\n```\n\n### Active Pattern for Parsing\n\n```scheme\n(import (chezscheme) (std match2))\n\n;; Active pattern: parse an ISO date string \"YYYY-MM-DD\"\n(define-active-pattern (iso-date s)\n  (and (string? s)\n       (= (string-length s) 10)\n       (char=? (string-ref s 4) #\\-)\n       (char=? (string-ref s 7) #\\-)\n       (let ([y (string->number (substring s 0 4))]\n             [m (string->number (substring s 5 7))]\n             [d (string->number (substring s 8 10))])\n         (and y m d (list y m d)))))\n\n(define (classify-date s)\n  (match s\n    [(iso-date y m d)\n     (cond\n       [(= m 1)  (format \"~a is in January\" y)]\n       [(= m 12) (format \"~a is in December\" y)]\n       [else     (format \"~a-~a-~a\" y m d)])]\n    [_ \"not a date\"]))\n\n(classify-date \"2024-01-15\")  ; => \"2024 is in January\"\n(classify-date \"2024-12-25\")  ; => \"2024 is in December\"\n(classify-date \"not-a-date\")  ; => \"not a date\"\n```\n\n### Exhaustive Match Catching Missing Cases\n\n```scheme\n(import (chezscheme) (std match2))\n\n(define-record-type (ok-val  make-ok  ok?)  (fields (immutable val ok-val)))\n(define-record-type (err-val make-err err?) (fields (immutable msg err-msg)))\n\n(define-sealed-hierarchy result\n  (ok-val  ok?  ok-val)\n  (err-val err? err-msg))\n\n;; Using match/strict signals that this should cover all variants.\n;; If you accidentally omit a branch and the missing case is hit at runtime,\n;; match raises an error:\n(define (unwrap r)\n  (match/strict result r\n    [(ok-val v) v]))\n\n;; This works fine when called with ok-val, but raises an error on err-val:\n;; (unwrap (make-err \"oops\"))\n;; => Error: match: no matching clause ...\n\n;; A complete version:\n(define (result->string r)\n  (match/strict result r\n    [(ok-val  v) (format \"ok: ~a\" v)]\n    [(err-val m) (format \"error: ~a\" m)]))\n\n(result->string (make-ok 42))        ; => \"ok: 42\"\n(result->string (make-err \"failed\")) ; => \"error: failed\"\n```\n\n---\n\n## Exhaustiveness Checking with `match/strict`\n\n`match/strict` is a documentation and defensive-programming convention. Its syntax is:\n\n```scheme\n(match/strict hierarchy-name expr clause ...)\n;; or, omitting the hierarchy name:\n(match/strict expr clause ...)\n```\n\nBoth forms compile identically to `match`. The difference is semantic intent: using `match/strict` with a `define-sealed-hierarchy` name signals that you expect all variants to be covered. If a variant is hit at runtime without a matching clause, `match` (and therefore `match/strict`) raises an error rather than silently returning an unspecified value.\n\nThis is distinct from compile-time exhaustiveness checking (which would require dependent types or a more complex macro system). Think of `match/strict` as the equivalent of a programmer assertion: \"I believe this match is complete; if it is not, fail loudly.\"\n\nWorkflow:\n\n1. Define your type hierarchy with `define-sealed-hierarchy`.\n2. Use `match/strict hier-name` for all dispatch over that hierarchy.\n3. When you add a new variant to the hierarchy, grep for `match/strict hier-name` to find all sites that need updating \u2014 and if any are missed, a runtime error with the unmatched value will identify the gap.\n\n---\n\n## Comparison with Standard Match\n\n| Feature | `(std match)` | `(std match2)` |\n|---------|---------------|----------------|\n| Wildcards and variables | Yes | Yes |\n| Literals (number, string, boolean, symbol) | Yes | Yes |\n| `(list ...)`, `(cons ...)`, `(vector ...)` | Yes | Yes |\n| `(? pred)` predicate | Yes | Yes |\n| `(? pred -> var)` predicate with binding | No | Yes |\n| `(=> proc var)` view pattern | No | Yes |\n| `(and ...)`, `(or ...)`, `(not ...)` | No | Yes |\n| `(box pat)` box patterns | No | Yes |\n| `(where guard)` inline guards | No | Yes |\n| Named struct patterns | No | Yes, via `define-match-type` |\n| Sealed hierarchies | No | Yes, via `define-sealed-hierarchy` |\n| Active (user-defined extractor) patterns | No | Yes, via `define-active-pattern` |\n| Strict/exhaustive mode | No | Yes, via `match/strict` |\n\nThe pattern dispatch in `match2` is runtime-based: `(TypeName ...)` patterns check a global hashtable to determine whether `TypeName` is an active pattern or a struct type. This means struct types and active patterns must be registered before any code that uses them runs (registration happens at module load time when using `define-match-type` or `define-active-pattern`).\n"}
 {"text": "<!-- FILE: docs/status.md -->\n# Jerboa Project Status\n\nUpdated 2026-03-21.\n\n## What Jerboa Is\n\nJerboa reimplements Gerbil Scheme's API surface as pure stock Chez Scheme\nlibraries \u2014 364 modules, ~2,900 tests, 13 fuzzing harnesses. No custom runtime,\nno patched Chez. The design prioritizes security, safety, and Claude-friendliness:\n`(jerboa prelude safe)` gives AI-generated code a safe-by-default environment with\ncontracts, structured concurrency, timeouts, and kernel sandboxing out of the box.\n\n---\n\n## Assessment Grades\n\n| Category | Grade | Notes |\n|----------|:---:|-------|\n| Security architecture | A+ | Allowlist sandbox, capabilities, taint, real Landlock + seccomp |\n| Safety (safe prelude) | A | Contracts, finalizers, SQL injection detection |\n| Safety (raw prelude) | B | No contracts, no guardian net, fork-thread exposed |\n| Claude-friendliness | A | Safe prelude is the recommended import |\n| Resource management | A | with-resource + guardian + structured concurrency |\n| Error diagnostics | A- | Full condition hierarchy, with-context, 14 lint rules |\n| Type safety | A- | Runtime + compile-time, fatal mode available |\n| Build integrity | B+ | Content-addressed artifacts + SBOM, no full orchestration |\n| Performance | B+ | Chez engines, Rust native backend, WPO |\n\n---\n\n## Implemented Features\n\n### Security\n\n| Module | Description |\n|--------|-------------|\n| `(std security sandbox)` | One-call sandbox: `run-safe` / `run-safe-eval` with fork-based isolation, Landlock + seccomp + capabilities + engine timeout |\n| `(std security restrict)` | Allowlist-only sandbox \u2014 113 bindings, no blocklist. `read` replaced with depth-limited `jerboa-read` (no `#.` read-eval) |\n| `(std security landlock)` | Kernel Landlock syscalls for filesystem sandboxing via foreign-alloc struct packing |\n| `(std security seccomp)` | BPF bytecode generation + seccomp(2) installation with architecture validation |\n| `(std security capability)` | Capability-based security with sealed opaque records + CSPRNG nonces \u2014 unforgeable by construction |\n| `(std security taint)` | Taint tracking with safe sinks \u2014 prevents untrusted data reaching `system`, `open-output-file`, etc. |\n| `(std security flow)` | Information flow control with label lattice |\n| `(std security privsep)` | Privilege separation via real fork(2) |\n| `(std secure-mem)` | mlock, guard pages, explicit_bzero, DONTDUMP, DONTFORK |\n| Rust native backend | Single `libjerboa_native.so` replaces 10+ C deps (ring for crypto, flate2, regex). `ffi_wrap()`/`set_last_error()` panic handling |\n| 13 fuzzing harnesses | Configurable depth limits across input parsers |\n| AI attack hardening | 13 findings addressed (empty-host = deny, symlink resolution, etc.) |\n\n### Safety\n\n| Module | Description |\n|--------|-------------|\n| `(std safe)` | Contract-checked stdlib: SQLite, TCP, File I/O, JSON with pre/post-condition checks. Runtime SQL injection heuristics. `*safe-mode*` parameter: `'check` or `'release` |\n| `(std resource)` | `with-resource` macro with LIFO cleanup via `dynamic-wind` |\n| `(std safe)` guardian net | Guardian-based finalizer safety net \u2014 warns when handles are GC'd without close, with best-effort cleanup |\n| `(std safe-timeout)` | `with-timeout` using Chez engines. `*default-timeout*` (30s) |\n| `(std safe-fasl)` | Rejects procedures, enforces record type registry, size limits, cycle detection |\n| `(jerboa prelude safe)` | Re-exports all safe APIs under standard names. Excludes `fork-thread` and raw FFI. Safe path is the default |\n\n### Error Handling\n\n| Module | Description |\n|--------|-------------|\n| `(std error conditions)` | Full condition hierarchy: `&jerboa` -> `&jerboa-network`, `&jerboa-db`, `&jerboa-actor`, `&jerboa-resource`, `&jerboa-timeout`, `&jerboa-serialization`, `&jerboa-parse` with subtypes |\n| `(std error context)` | `with-context` for automatic context accumulation in error diagnostics |\n\n### Types\n\n| Module | Description |\n|--------|-------------|\n| `(std typed)` + `(std typed env)` + `(std typed infer)` | Gradual type system with bidirectional inference, subtyping, 100+ builtins |\n| `(std typed check)` | Compile-time type checking via expand-time inference. `*type-errors-fatal*` for strict mode |\n| Refinement, linear, affine, phantom, GADTs, typeclasses, row polymorphism, HKTs | `define/r` and `lambda/r` for checked refinements |\n\n### Concurrency\n\n| Module | Description |\n|--------|-------------|\n| `(std concur structured)` | `with-task-scope`, `scope-spawn`, `task-await`, `task-cancel`, `parallel`, `race` |\n| `(std concur deadlock)` | Runtime deadlock detection via wait-for graph + DFS cycle detection. `make-checked-mutex`, `with-checked-mutex` |\n| `(std concur)` annotations | Thread-safety annotations with eq-hashtable tracking |\n| `(std actor bounded)` | `spawn-bounded-actor` with 10,000 message default capacity. Strategies: `'block`, `'drop`, `'error` |\n\n### Build\n\n| Module | Description |\n|--------|-------------|\n| `(build reproducible)` | SHA-256 content hashing (FNV-1a fallback), safe `mkdir-p`, content-addressed artifact store |\n| `(build sbom)` | Cargo.lock + Cargo.toml parsers for Rust dep detection. `detect-all-deps` aggregates Scheme + C + Rust into SBOM |\n\n### Text Processing \u2014 Regex, Patterns, PEG\n\n| Module | Description |\n|--------|-------------|\n| `(std regex)` | Unified regex facade: `re`, `re-match?`, `re-search`, `re-find-all`, `re-replace`, `re-split`, `re-fold`, named captures via SRE `(=> name ...)`. Auto-selects Rust native or pregexp backend |\n| `(std rx)` | Composable pattern macros: `rx` compiles SRE inline, `define-rx` creates named patterns that can be spliced into later definitions. SRE keyword shadowing prevented |\n| `(std rx patterns)` | 30+ pre-built patterns: IPv4, email, UUID, semver, ISO 8601 date/time, identifiers, hex colors, numbers, quoted strings, URLs, JWT, MAC addresses |\n| `(std peg)` | PEG grammar system with packrat memoization: `define-grammar` macro, named captures, `drop`, `sep-by`, complement, mutual recursion. O(n) per rule |\n| `(std srfi srfi-115)` | SRFI-115 SRE compiler extended with `(embed ...)` for pattern splicing, `sre->named-groups` for capture tracking, char-class union via `(or ...)` inside `(~ ...)` |\n| `#r\"...\"` raw strings | Jerboa reader syntax: backslashes are literal, no escape processing. Eliminates regex escape hell |\n\n### Linting\n\n| Module | Description |\n|--------|-------------|\n| `(std lint)` | 14 rules: `unused-define`, `shadowed-define`, `redefine-builtin`, `unsafe-import`, `duplicate-import`, `unused-only-import`, `bare-error`, `sql-interpolation`, `empty-begin`, `single-arm-cond`, `missing-else`, `deep-nesting`, `long-lambda`, `magic-number` |\n\n### Decompression Bomb Protection\n\n| Input | Limit |\n|-------|-------|\n| JSON | `*json-max-depth*` (512), `*json-max-string-length*` (10 MB) |\n| XML | `*sxml-max-depth*` (512), `*sxml-max-output-size*` (50 MB) |\n| FASL | `*fasl-max-object-count*` (1M), `*fasl-max-byte-size*` (100 MB) |\n| Zlib | 100 MB decompression limit in Rust native backend |\n\n---\n\n## Known Limitations\n\n| Module | Limitation | Reason |\n|--------|-----------|--------|\n| Landlock | x86_64 only, requires Linux 5.13+ | Syscall numbers are architecture-specific |\n| Seccomp | x86_64 only, filters are irreversible | BPF bytecode is architecture-specific by design |\n| typed/check | Compile-time checks are warnings by default | Gradual typing standard practice; use `*type-errors-fatal*` for strict |\n| build/reproducible | Falls back to FNV-1a if `(std crypto hash)` unavailable | SHA-256 requires the crypto module |\n| concur/deadlock | Opt-in via `make-checked-mutex` | Auto-instrumenting all mutexes would break existing code |\n| Lint | No full unbound-identifier detection or call-site arity checking | Requires compile-time analysis beyond static linting |\n| Build | No bit-identical reproducible build verification end-to-end | Full build orchestration pipeline not implemented |\n\n---\n\n## Remaining Work\n\n1. **Race detector** \u2014 No dynamic race detection. Structured concurrency reduces risk but does not eliminate it for code using raw `fork-thread` via `(jerboa prelude)`.\n2. **Full build orchestration pipeline** \u2014 Content-addressed artifacts and SBOM exist, but end-to-end reproducible build orchestration is not wired up.\n\n---\n\n## Stub/Partial Audit Result\n\nOriginal audit found 2 stubs and 6 partials. **All are now fixed (0 stubs remaining).**\n\n| Module | Was | Resolution |\n|--------|-----|------------|\n| security/landlock | STUB (no kernel calls) | Real foreign-alloc struct packing + real syscalls |\n| security/seccomp | STUB (no BPF generation) | Real BPF bytecode generator + seccomp(2) install |\n| typed/check | PARTIAL (phase error, warnings only) | Fixed phase imports, added `*type-errors-fatal*` |\n| concur/deadlock | PARTIAL (no drop-in replacements) | Added `make-checked-mutex`, `with-checked-mutex` |\n| build/reproducible | PARTIAL (shell injection, weak hash) | Safe `mkdir-p`, SHA-256 with fallback |\n| build/sbom | PARTIAL (no Rust deps) | Cargo.lock + Cargo.toml parsers |\n| concur | PARTIAL (no distributed) | Was always real for local use; documented as limitation |\n\n---\n\n## Key Differentiators vs. Other Languages for Claude\n\n1. Allowlist sandbox \u2014 best-in-class for running AI-generated code safely\n2. Capability-based security \u2014 unforgeable by construction\n3. Chez engines \u2014 preemptive timeout without OS signals\n4. Taint tracking \u2014 prevents injection from untrusted sources\n5. Rust native backend \u2014 memory-safe FFI without C footguns\n6. Safe-by-default prelude \u2014 Claude gets safety without asking for it\n7. Real Landlock + seccomp \u2014 kernel-enforced sandboxing (Linux 5.13+)\n"}
-{"text": "<!-- FILE: docs/quickstart.md -->\n# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs\n"}
+{"text": "<!-- FILE: docs/quickstart.md -->\n# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs\n"}
 {"text": "<!-- FILE: docs/async.md -->\n# `(std async)` \u2014 Async I/O Runtime\n\n## Overview\n\n`(std async)` provides a structured-concurrency runtime for Jerboa (a Chez Scheme dialect) built on top of the algebraic effects system in `(std effect)`. It gives you cooperative, task-based async programming without abandoning native threads \u2014 instead it uses threads as the underlying suspension mechanism while exposing a clean effect-based API.\n\n### Thread-per-task model\n\nEach async task runs in its own OS thread. Suspension is real thread blocking:\n\n- `(Async await promise)` \u2014 blocks the current thread via a mutex/condition variable until the promise resolves\n- `(Async spawn thunk)` \u2014 forks a new OS thread with Async handlers pre-installed, then returns immediately to the caller\n- `(Async sleep ms)` \u2014 calls Chez Scheme's `sleep` on the current thread\n\nThis is distinct from a cooperative event loop (like Node.js or Tokio): there is no central scheduler and no `poll`-based reactor. Concurrency comes from OS-level thread scheduling. The benefit is that truly blocking C calls (filesystem, sockets via FFI) work without any special adaptation.\n\n### Built on algebraic effects\n\nThe entire suspension protocol is expressed as the `Async` algebraic effect (defined with `defeffect` from `(std effect)`). Effect handlers installed by `run-async` intercept each `Async` operation and implement it with mutex/condition-variable primitives from Chez Scheme's built-in thread library. You can write your own handlers on top of `Async::descriptor` to intercept, log, or mock async operations in tests.\n\n### Dependencies\n\n```\n(std async) imports:\n  (chezscheme)       \u2014 fork-thread, mutex, condition, sleep\n  (std effect)       \u2014 defeffect, with-handler, resume\n  (std misc channel) \u2014 channel-get, channel-put, channel-try-get\n```\n\n---\n\n## Core API\n\n### Effect descriptor\n\n```scheme\nAsync::descriptor\n```\n\nThe unique `effect-descriptor` record identifying the `Async` effect. You rarely reference this directly, but it is exported so that external code can install custom handlers with `with-handler` using the `Async` name, or introspect the handler stack via `*effect-handlers*`.\n\n---\n\n### `Async` \u2014 effect operations\n\n`Async` is a macro generated by `defeffect`. It has three operations:\n\n#### `(Async await promise)`\n\nSuspend the current task until `promise` is resolved, then return its value. Internally calls `promise-wait!` which performs `condition-wait` in a loop \u2014 a true blocking wait on the OS thread.\n\nReturns the value passed to `async-promise-resolve!`.\n\n```scheme\n(run-async\n  (lambda ()\n    (let ([p (make-async-promise)])\n      ;; Resolve the promise from a spawned task\n      (Async spawn (lambda () (async-promise-resolve! p 42)))\n      ;; Suspend until resolved\n      (Async await p)))   ;=> 42\n```\n\n#### `(Async spawn thunk)`\n\nLaunch `thunk` as a concurrent task in a new OS thread. Returns `(void)` immediately; the caller is not blocked. The new thread runs with the same Async handler infrastructure installed.\n\n```scheme\n(Async spawn (lambda ()\n               (display \"running concurrently\\n\")))\n```\n\n#### `(Async sleep ms)`\n\nSleep the current task thread for `ms` milliseconds. Uses Chez Scheme's `sleep` with a `time-duration` record constructed from the millisecond count.\n\n```scheme\n(Async sleep 500)   ; sleep 500 ms\n```\n\n---\n\n### `run-async`\n\n```scheme\n(run-async thunk) => value\n```\n\nEntry point for async code. Runs `thunk` in a new thread with Async effect handlers installed, blocks the calling thread until the thunk completes, and returns the thunk's result.\n\n- Installs the three Async handlers (`await`, `spawn`, `sleep`) via `install-async-handlers!`.\n- Wraps the thunk in a `guard` that catches any exception, prints it to `current-error-port`, and re-raises it so the main thread sees the failure (the promise is resolved with `raise-continuable`).\n- The main thread blocks on `promise-wait!` \u2014 a `condition-wait` loop \u2014 until the root task finishes.\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n(run-async\n  (lambda ()\n    (display \"hello from async\\n\")\n    42))\n;=> 42\n```\n\n---\n\n### `run-async/workers`\n\n```scheme\n(run-async/workers thunk n-workers) => value\n```\n\nIdentical to `run-async` in behavior. The `n-workers` argument is accepted for API compatibility but has no effect: since each `(Async spawn ...)` call creates a dedicated OS thread, there is no fixed worker pool to size. Future versions may use `n-workers` to cap parallelism via a semaphore.\n\n```scheme\n(run-async/workers my-thunk 4)   ; n-workers is currently ignored\n```\n\n---\n\n### Promises\n\nPromises are the fundamental coordination primitive. A promise is a write-once, broadcast-notify cell backed by a mutex and condition variable.\n\n#### `(make-async-promise)` => promise\n\nCreate a new, unresolved promise.\n\n```scheme\n(define p (make-async-promise))\n```\n\n#### `(async-promise? x)` => boolean\n\nReturn `#t` if `x` is an async promise.\n\n#### `(async-promise-resolve! p val)`\n\nResolve promise `p` with value `val`. Thread-safe. If `p` is already resolved, the call is a no-op (the first write wins). All threads waiting on `p` via `(Async await p)` are unblocked via `condition-broadcast`.\n\n```scheme\n(async-promise-resolve! p 'done)\n```\n\n#### `(async-promise-resolved? p)` => boolean\n\nReturn `#t` if the promise has been resolved. This is a non-blocking poll; it does not synchronize with the condition variable beyond the mutex acquisition implied by the record field read.\n\n#### `(async-promise-value p)` => value\n\nReturn the resolved value. Behavior is unspecified if called before the promise is resolved; use `(async-promise-resolved? p)` or `(Async await p)` to ensure resolution first.\n\n---\n\n### Task management\n\n#### `(async-task thunk)` => promise\n\nSpawn a concurrent task that runs `thunk` and captures its return value in a new promise. Returns the promise immediately. The task is launched via `(Async spawn ...)`, so this must be called inside an `run-async` context.\n\n```scheme\n(run-async\n  (lambda ()\n    (let ([p (async-task (lambda () (expensive-computation)))])\n      ;; ... do other work ...\n      (Async await p))))  ; block until computation finishes\n```\n\n#### `(async-task? x)` => boolean\n\nAlias for `async-promise?`. An `async-task` handle is just an `async-promise`.\n\n---\n\n### Async channels\n\nAsync channels bridge `(std misc channel)` (which uses OS-level blocking internally) with the `Async` effect so that waiting for channel data suspends via `(Async await ...)` rather than blocking the thread with a raw `channel-get`.\n\n#### `(async-channel-get ch)` => value\n\nGet the next value from channel `ch`. If the channel already has data, it is returned immediately via `channel-try-get`. If the channel is empty, a helper thread is spawned that blocks on `channel-get`, and the current task suspends via `(Async await promise)` until the helper thread delivers a value.\n\n#### `(async-channel-put ch val)`\n\nPut `val` into channel `ch`. Always asynchronous: a helper thread is forked to call `channel-put` (which may block if `ch` is bounded and full), and the current task suspends via `(Async await promise)` until the put completes.\n\n> **Note:** Both channel operations spawn a helper OS thread per call. For high-throughput channels, prefer using `channel-get`/`channel-put` directly from dedicated producer/consumer threads rather than through the Async channel wrappers.\n\n---\n\n### `async-sleep`\n\n```scheme\n(async-sleep ms)\n```\n\nSleep the current async task for `ms` milliseconds. Thin wrapper around `(Async sleep ms)`. Must be called inside a `run-async` context.\n\n```scheme\n(run-async\n  (lambda ()\n    (display \"before\\n\")\n    (async-sleep 1000)\n    (display \"after 1 second\\n\")))\n```\n\n---\n\n## Working examples\n\n### 1. Basic async task with promise\n\nSpawn a computation, await its result.\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n(run-async\n  (lambda ()\n    (let ([p (async-task (lambda ()\n                           ;; Simulate work\n                           (async-sleep 100)\n                           (* 6 7)))])\n      (let ([result (Async await p)])\n        (printf \"Result: ~a~%\" result)))))\n; prints: Result: 42\n```\n\n### 2. Parallel tasks with join\n\nRun two independent computations in parallel and join their results.\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n(define (parallel-add a b)\n  (run-async\n    (lambda ()\n      (let ([pa (async-task (lambda () (begin (async-sleep 50) a))))\n            [pb (async-task (lambda () (begin (async-sleep 80) b)))])\n        ;; Both tasks run concurrently; total wall time ~80 ms, not 130 ms\n        (+ (Async await pa) (Async await pb))))))\n\n(printf \"~a~%\" (parallel-add 10 32))   ; => 42\n```\n\n### 3. Producer/consumer with async channels\n\n```scheme\n(import (chezscheme) (std effect) (std async) (std misc channel))\n\n(run-async\n  (lambda ()\n    (let ([ch (make-channel 4)])   ; bounded channel, capacity 4\n\n      ;; Producer: send 5 items then a sentinel\n      (Async spawn\n        (lambda ()\n          (let loop ([i 0])\n            (when (< i 5)\n              (async-channel-put ch i)\n              (async-sleep 10)\n              (loop (+ i 1))))\n          (async-channel-put ch 'done)))\n\n      ;; Consumer: drain until sentinel\n      (let consume ()\n        (let ([msg (async-channel-get ch)])\n          (unless (eq? msg 'done)\n            (printf \"got: ~a~%\" msg)\n            (consume))))\n\n      (display \"consumer finished\\n\"))))\n```\n\n### 4. HTTP-like request with timeout\n\nPattern for racing an async operation against a deadline.\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n(define (with-timeout ms thunk)\n  ;; Returns (values result #t) on success or (values #f #f) on timeout\n  (run-async\n    (lambda ()\n      (let ([result-p  (async-task thunk)]\n            [timeout-p (make-async-promise)])\n        ;; Timeout task\n        (Async spawn\n          (lambda ()\n            (async-sleep ms)\n            (async-promise-resolve! timeout-p 'timeout)))\n        ;; Race: whichever promise resolves first wins\n        ;; We poll both \u2014 a proper select would use channel-select\n        (let poll ()\n          (cond\n            [(async-promise-resolved? result-p)\n             (values (async-promise-value result-p) #t)]\n            [(async-promise-resolved? timeout-p)\n             (values #f #f)]\n            [else\n             (async-sleep 5)\n             (poll)]))))))\n\n(define-values (val ok)\n  (with-timeout 200\n    (lambda ()\n      (async-sleep 100)   ; fast enough\n      \"response body\")))\n\n(if ok\n  (printf \"Got: ~a~%\" val)\n  (printf \"Timed out~%\"))\n```\n\n### 5. Fan-out / gather pattern\n\nDispatch N tasks in parallel and collect all results.\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n(define (gather thunks)\n  ;; Returns a list of results in the same order as thunks\n  (run-async\n    (lambda ()\n      (let ([promises (map async-task thunks)])\n        (map (lambda (p) (Async await p)) promises)))))\n\n(define results\n  (gather\n    (list\n      (lambda () (begin (async-sleep 30) 'a))\n      (lambda () (begin (async-sleep 10) 'b))\n      (lambda () (begin (async-sleep 20) 'c)))))\n\n(display results)   ; => (a b c)\n; Total wall time ~30 ms, not 60 ms\n```\n\n---\n\n## Error handling\n\nWhen a task spawned by `async-task` or `(Async spawn ...)` throws an unhandled exception:\n\n- **`async-task`**: the exception propagates inside the spawned thread. Because `async-task` does not wrap the thunk in a `guard`, the exception will be printed by Chez Scheme's default thread-error handler and the task's promise will **never resolve**. Any `(Async await p)` on that promise will block forever.\n\n  Defensive pattern \u2014 wrap the thunk yourself:\n\n  ```scheme\n  (define (safe-task thunk)\n    (let ([p (make-async-promise)])\n      (Async spawn\n        (lambda ()\n          (guard (exn [#t (async-promise-resolve! p (cons 'error exn))])\n            (async-promise-resolve! p (cons 'ok (thunk))))))\n      p))\n  ```\n\n- **`run-async` root task**: the `guard` in `run-async` catches any exception thrown by the root thunk, prints it to `current-error-port`, and resolves the result promise with `raise-continuable`. This means the exception is re-raised in the calling thread (the thread that called `run-async`), so the caller sees a normal Chez Scheme condition.\n\n  ```scheme\n  (guard (exn [#t (printf \"caught: ~a~%\" (condition-message exn))])\n    (run-async (lambda () (error \"boom\" \"test\"))))\n  ; stderr: run-async error: boom\n  ; caught: boom\n  ```\n\n---\n\n## Performance\n\n| Scenario | Recommendation |\n|---|---|\n| CPU-bound parallel work | Use `async-task` / `gather` \u2014 each gets a real OS thread |\n| Many short-lived tasks | Overhead per task is `fork-thread` cost; avoid spawning thousands |\n| Blocking FFI/IO | Works naturally \u2014 threads block independently |\n| High-throughput channels | Use `channel-get`/`channel-put` directly from dedicated threads; avoid `async-channel-get`/`async-channel-put` (each call spawns a helper thread) |\n| Sequential async code | `run-async` with `async-sleep` / `Async await` is fine; no event-loop overhead |\n\n`run-async/workers n` currently does not limit parallelism. If you need to cap OS thread count (e.g., for a bounded thread pool), implement a semaphore-based wrapper around `async-task`.\n\n---\n\n## Integration with `(std effect)`\n\n`Async` is defined with `defeffect`, which generates:\n\n1. `Async::descriptor` \u2014 a unique `effect-descriptor` record used as the key in the handler hashtable.\n2. A macro `Async` that expands `(Async op arg ...)` into `(effect-perform Async::descriptor 'op (list arg ...))`.\n\n`effect-perform` captures the current one-shot continuation (`call/1cc`) and calls the matching handler procedure with `(k . args)`. The handler can then:\n\n- Call `(resume k val)` to continue the task with `val` as the result of the `Async` expression.\n- Block the current thread before resuming (as the `await` and `sleep` handlers do).\n- Fork a new thread and resume immediately (as the `spawn` handler does).\n\nThe handler stack is stored in a thread-local parameter `*effect-handlers*` (a list of `eq-hashtable` frames). Each `with-handler` call pushes a new frame; `run-with-handler` installs it via `parameterize`. Because each spawned task runs in its own thread, `fork-thread` inherits the parent's thread-parameter values, so child tasks start with the same handler stack depth \u2014 but `install-async-handlers!` then installs a fresh set of Async handlers on top for the new thread.\n\n### Replacing handlers for testing\n\nYou can intercept `Async` operations in unit tests by wrapping code in `with-handler` before it reaches the default handlers:\n\n```scheme\n(import (chezscheme) (std effect) (std async))\n\n;; Mock: record all sleep durations, never actually sleep\n(define recorded-sleeps '())\n\n(with-handler\n  ([Async\n    (sleep (k ms)\n      (set! recorded-sleeps (cons ms recorded-sleeps))\n      (resume k (void)))])   ; override: skip the real sleep\n  (run-async\n    (lambda ()\n      (async-sleep 100)\n      (async-sleep 200))))\n\n(display recorded-sleeps)   ; => (200 100)\n```\n\nNote that `with-handler` pushes a frame on top of the stack, so the inner `with-handler` in the example above takes precedence over the Async handlers installed by `run-async`, because `effect-perform` searches from the innermost frame outward.\n\n---\n\n## Summary of exported symbols\n\n| Symbol | Kind | Purpose |\n|---|---|---|\n| `Async` | macro | Perform `await`, `spawn`, or `sleep` operations |\n| `Async::descriptor` | value | Effect descriptor for the `Async` effect |\n| `run-async` | procedure | Run a thunk in async context, block until done |\n| `run-async/workers` | procedure | Like `run-async`; `n-workers` arg is a no-op |\n| `make-async-promise` | procedure | Create an unresolved promise |\n| `async-promise?` | procedure | Type predicate |\n| `async-promise-resolve!` | procedure | Resolve a promise (write-once, thread-safe) |\n| `async-promise-resolved?` | procedure | Non-blocking poll for resolution |\n| `async-promise-value` | procedure | Read the resolved value |\n| `async-task` | procedure | Spawn a task, return its result promise |\n| `async-task?` | procedure | Alias for `async-promise?` |\n| `async-channel-get` | procedure | Get from channel, suspending via `Async await` |\n| `async-channel-put` | procedure | Put to channel, suspending via `Async await` |\n| `async-sleep` | procedure | Sleep current task for N milliseconds |\n"}
 {"text": "<!-- FILE: docs/native-rust.md -->\n# Replacing C Dependencies with a Unified Rust Native Library\n\nJerboa's C library dependencies are being replaced with a single Rust shared library (`libjerboa_native.so`). Rust implementations are complete for crypto, compression, regex, databases, and OS integration. TLS (rustls) and LevelDB are still pending. The legacy chez-* C wrappers remain available as fallbacks. Every Rust module is callable from Chez Scheme via the same `foreign-procedure` FFI.\n\n---\n\n## Table of Contents\n\n1. [Motivation](#motivation)\n2. [Current C Dependencies](#current-c-dependencies)\n3. [Rust Replacements](#rust-replacements)\n4. [Architecture](#architecture)\n5. [C ABI Design](#c-abi-design)\n6. [FFI Binding Patterns](#ffi-binding-patterns)\n7. [Secure Memory Region](#secure-memory-region)\n8. [What Each Swap Fixes](#what-each-swap-fixes)\n9. [Build Integration](#build-integration)\n10. [Migration Strategy](#migration-strategy)\n11. [Implementation Roadmap](#implementation-roadmap)\n\n---\n\n## Motivation\n\nJerboa currently links 9+ C shared libraries for crypto, TLS, compression, regex, databases, and OS integration. (YAML was previously a C dependency but is now pure Scheme.) Each C library is an independent attack surface: its own CVE history, its own build system, its own ABI compatibility concerns.\n\nReplacing them with a single Rust shared library (`libjerboa_native.so`) provides:\n\n1. **Memory safety** across all native code \u2014 buffer overflows, use-after-free, and double-free become compile-time errors\n2. **A single dependency** instead of 10+ \u2014 one build, one audit surface, one update path\n3. **Solved vulnerability classes** \u2014 ReDoS eliminated (Rust regex uses NFA, not backtracking), timing-safe crypto by default (ring). (YAML parsing is now pure Scheme \u2014 no C/Rust needed.)\n4. **Stable C ABI** \u2014 Rust's `extern \"C\"` produces `.so` files that Chez Scheme loads identically to C libraries\n5. **Reproducible builds** \u2014 Cargo.lock pins every transitive dependency with exact hashes\n\n---\n\n## Current C Dependencies\n\n### Required\n\n| Library          | Module(s)                      | Purpose                                                     |\n|------------------|--------------------------------|-------------------------------------------------------------|\n| **glibc / musl** | `std/os/*`, `std/misc/process` | Core OS: mkstemp, getpid, kill, mmap, dup2, signal handling |\n\n### Crypto and Network\n\n| Library                    | Module(s)                                                     | Purpose                                                                                      |\n|----------------------------|---------------------------------------------------------------|----------------------------------------------------------------------------------------------|\n| ~~**libcrypto.so** (OpenSSL)~~ | ~~`std/crypto/native`~~  | **Eliminated** \u2014 Digests, HMAC, AES-GCM, PBKDF2, CSPRNG now via ring in `libjerboa_native` |\n| ~~**libssl.so** (OpenSSL)~~    | ~~`std/net/tls`~~        | **Eliminated** \u2014 TLS 1.2/1.3 now via rustls in `libjerboa_native`                           |\n\n### Optional Wrappers (chez-* repos)\n\n| Library                    | Shim            | Module              | Purpose                     |\n|----------------------------|-----------------|---------------------|-----------------------------|\n| **libz.so**                | chez-zlib       | `std/compress/zlib` | Deflate/inflate compression |\n| **libpcre2-8.so**          | chez-pcre2      | `std/pcre2`         | Perl-compatible regex       |\n| **libsqlite3.so**          | chez-sqlite     | `std/db/sqlite`     | SQLite database             |\n| **libpq.so**               | chez-postgresql | `std/db/postgresql` | PostgreSQL client           |\n| **libleveldb.so**          | chez-leveldb    | `std/db/leveldb`    | LevelDB key-value store     |\n| ~~**libyaml.so**~~         | ~~chez-yaml~~   | `std/text/yaml`     | ~~YAML parsing~~ \u2014 **Eliminated**: now pure Scheme with roundtrip support |\n| **libcurl.so** (or libssl) | chez-https      | `std/net/request`   | HTTPS client                |\n\n### Custom C Code\n\n| File                        | Module            | Purpose                                     |\n|-----------------------------|-------------------|---------------------------------------------|\n| **support/landlock-shim.c** | `std/os/landlock` | Landlock LSM syscall wrapper                |\n| **support/jerboa-embed.c**  | `jerboa/embed`    | C API for embedding Chez in C/Rust programs |\n\n### Linux-Specific (No External Library)\n\n| Shim              | Module           | Purpose                         |\n|-------------------|------------------|---------------------------------|\n| chez-epoll        | `std/os/epoll`   | Event polling (direct syscalls) |\n| chez-inotify      | `std/os/inotify` | File watching (direct syscalls) |\n| liburing-ffi.so.2 | `std/os/iouring` | io_uring async I/O              |\n\n---\n\n## Rust Replacements\n\n### Crypto: libcrypto \u2192 ring\n\n| Feature               | Current (OpenSSL)                         | Replacement (ring)                             | Notes                                        |\n|-----------------------|-------------------------------------------|------------------------------------------------|----------------------------------------------|\n| Digests               | `EVP_DigestInit/Update/Final`             | `ring::digest`                                 | SHA-1, SHA-256, SHA-384, SHA-512             |\n| HMAC                  | `HMAC()`                                  | `ring::hmac`                                   | All SHA variants                             |\n| AES-GCM               | `EVP_Encrypt/Decrypt` + `EVP_aes_256_gcm` | `ring::aead`                                   | AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305  |\n| CSPRNG                | `RAND_bytes()`                            | `ring::rand::SystemRandom`                     | Uses OS entropy source                       |\n| PBKDF2                | `PKCS5_PBKDF2_HMAC()`                     | `ring::pbkdf2`                                 | Timing-safe verification built in            |\n| Constant-time compare | `CRYPTO_memcmp()`                         | `ring::constant_time::verify_slices_are_equal` | Default behavior, not opt-in                 |\n| MD5                   | `EVP_md5()`                               | **Not in ring** \u2014 use `md-5` crate             | ring deliberately excludes broken algorithms |\n\n**ring** is audited, powers rustls and Cloudflare's infrastructure, and has no `unsafe` in its Rust API. It uses BoringSSL's C/asm for primitives internally (AES-NI, constant-time implementations) but presents a safe Rust interface.\n\n**Alternative**: RustCrypto crates (`sha2`, `aes-gcm`, `hmac`, `pbkdf2`) \u2014 pure Rust, no C/asm. Slightly slower but fully auditable Rust source. Good for environments where \"no C at all\" is the goal.\n\n### TLS: libssl \u2192 rustls-ffi\n\n| Feature                  | Current (OpenSSL)                    | Replacement (rustls)                                | Notes                                                   |\n|--------------------------|--------------------------------------|-----------------------------------------------------|---------------------------------------------------------|\n| TLS 1.2/1.3              | `SSL_CTX_new`, `SSL_connect`, etc.   | `rustls_client_config_builder`, `rustls_connection` | Full protocol support                                   |\n| Certificate verification | OpenSSL's X.509 stack                | `webpki` + `rustls-native-certs`                    | Uses OS trust store                                     |\n| Hostname verification    | Manual or `SSL_set_tlsext_host_name` | Automatic and mandatory                             | Cannot be disabled \u2014 safe by default                    |\n| Cipher suites            | Configurable (including weak ones)   | Only safe suites, no configuration needed           | TLS_AES_256_GCM, TLS_CHACHA20_POLY1305, TLS_AES_128_GCM |\n\n**rustls-ffi** already exists as a C ABI library specifically designed for non-Rust consumers. It provides a stable C header (`rustls.h`) that Chez can call directly. This is not a hypothetical \u2014 curl, Apache httpd, and other C projects already use rustls-ffi in production.\n\nZero memory-safety CVEs in rustls, compared to hundreds in OpenSSL.\n\n### Regex: libpcre2 \u2192 regex crate\n\n| Feature | Current (PCRE2) | Replacement (regex) | Notes |\n|---------|-----------------|---------------------|-------|\n| Pattern matching | Backtracking NFA | Thompson NFA (finite automata) | **Guaranteed linear-time** \u2014 ReDoS impossible |\n| Unicode | Full Unicode support | Full Unicode support | `\\p{Lu}`, `\\w`, etc. |\n| Capture groups | Named and numbered | Named and numbered | Same API surface |\n| JIT compilation | PCRE2 JIT | Lazy DFA compilation | Comparable performance |\n\nThe Rust regex crate **eliminates ReDoS entirely**. Pathological patterns like `(a+)+b` that hang PCRE2/pregexp complete in microseconds because the engine cannot backtrack. This single swap fixes the entire ReDoS section of security2.md for any code path that uses the native regex engine.\n\n### Compression: libz \u2192 flate2\n\n| Feature | Current (zlib) | Replacement (flate2) | Notes |\n|---------|---------------|---------------------|-------|\n| Deflate | `deflate()` / `inflate()` | `flate2::Compress` / `flate2::Decompress` | Compatible output |\n| Gzip | `gz*` functions | `flate2::read::GzDecoder` | Same format |\n\nflate2 uses `miniz_oxide` (pure Rust) by default. No C code, no unsafe. Performance is within 10% of zlib.\n\n**Decompression bomb protection**: Easy to add a size limit in the Rust wrapper:\n\n```rust\n#[no_mangle]\npub extern \"C\" fn jerboa_inflate(\n    input: *const u8, input_len: usize,\n    output: *mut u8, output_max: usize,\n    output_len: *mut usize\n) -> i32 {\n    // output_max caps decompression \u2014 no billion-byte bombs\n}\n```\n\n### ~~YAML: libyaml \u2192 unsafe-libyaml~~ \u2014 SUPERSEDED\n\n**This swap is no longer needed.** `(std text yaml)` is now a pure Scheme implementation with full roundtrip support (preserves comments, key ordering, scalar styles, block/flow collection styles). No C dependency, no FFI \u2014 the entire libyaml attack surface has been eliminated without needing a Rust replacement.\n\nThe pure Scheme implementation provides:\n- Simple API: `yaml-load`, `yaml-dump` (backward-compatible with old chez-yaml interface)\n- Roundtrip API: `yaml-read`, `yaml-write` (returns/consumes AST nodes with comment and style metadata)\n- Node manipulation: `yaml-mapping-ref`, `yaml-mapping-set!`, `yaml-ref`, `yaml-set!`\n- Security: `*yaml-max-input-size*` (1MB) and `*yaml-max-depth*` (512) limits\n\n### Databases: libsqlite3 \u2192 rusqlite, libpq \u2192 rust-postgres\n\n| Current | Replacement | Notes |\n|---------|-------------|-------|\n| libsqlite3 (via chez-sqlite) | **rusqlite** | Bundles SQLite source or links system lib. Parameterized queries by default. |\n| libpq (via chez-postgresql) | **rust-postgres** | Pure Rust PostgreSQL client. TLS via rustls. No C dependency. |\n| libleveldb (via chez-leveldb) | **rusty-leveldb** or **sled** | rusty-leveldb is API-compatible; sled is pure Rust with different (better) API |\n\n### OS Integration\n\n| Current | Replacement | Notes |\n|---------|-------------|-------|\n| landlock-shim.c | **landlock** crate | Clean Rust API for Landlock ABI v1-v4 |\n| liburing-ffi.so.2 | **io-uring** crate | Safe wrapper over kernel interface |\n| chez-epoll shim | Rust `epoll` wrapper or **mio** | mio abstracts over epoll/kqueue/IOCP |\n| chez-inotify shim | **inotify** crate | Safe wrapper |\n\n---\n\n## Architecture\n\n### Single Shared Library\n\nAll Rust code compiles into one shared library:\n\n```\njerboa-native-rs/\n\u251c\u2500\u2500 Cargo.toml\n\u251c\u2500\u2500 cbindgen.toml              # generates jerboa_native.h\n\u251c\u2500\u2500 src/\n\u2502   \u251c\u2500\u2500 lib.rs                 # top-level: panic handler, init\n\u2502   \u251c\u2500\u2500 crypto.rs              # ring: digest, hmac, aead, csprng, pbkdf2\n\u2502   \u251c\u2500\u2500 tls.rs                 # rustls-ffi: connect, accept, read, write\n\u2502   \u251c\u2500\u2500 regex.rs               # regex crate: compile, match, find, replace\n\u2502   \u251c\u2500\u2500 compress.rs            # flate2: deflate, inflate (with size limits)\n\u2502   \u251c\u2500\u2500 sqlite.rs              # rusqlite: open, prepare, bind, step, finalize\n\u2502   \u251c\u2500\u2500 postgres.rs            # rust-postgres: connect, query, execute\n\u2502   \u251c\u2500\u2500 secure_mem.rs          # mlock, guard pages, explicit_bzero, DONTDUMP\n\u2502   \u251c\u2500\u2500 landlock.rs            # landlock crate: create ruleset, add rules, enforce\n\u2502   \u251c\u2500\u2500 iouring.rs             # io-uring crate: queue init, submit, wait\n\u2502   \u251c\u2500\u2500 epoll.rs               # epoll: create, ctl, wait\n\u2502   \u251c\u2500\u2500 inotify.rs             # inotify: init, add_watch, read_events\n\u2502   \u2514\u2500\u2500 panic.rs               # catch_unwind wrapper for all extern \"C\" functions\n\u251c\u2500\u2500 tests/\n\u2502   \u2514\u2500\u2500 ffi_test.rs            # test C ABI from Rust side\n\u2514\u2500\u2500 target/release/\n    \u2514\u2500\u2500 libjerboa_native.so    # single output: ~2-5MB\n```\n\n```toml\n# Cargo.toml\n[package]\nname = \"jerboa-native\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nring = \"0.17\"\nrustls = \"0.23\"\nrustls-pemfile = \"2\"\nwebpki-roots = \"0.26\"\nregex = \"1\"\nflate2 = \"1\"\n# unsafe-libyaml \u2014 no longer needed: (std text yaml) is pure Scheme\nrusqlite = { version = \"0.32\", features = [\"bundled\"] }\npostgres = \"0.19\"\nlandlock = \"0.4\"\nio-uring = \"0.7\"\ninotify = \"0.11\"\nlibc = \"0.2\"\n```\n\n### Why One Library, Not Many\n\n- **One `load-shared-object` call** in Jerboa \u2014 no hunting for 10 different `.so` files\n- **One build step** \u2014 `cargo build --release` produces everything\n- **One audit surface** \u2014 `cargo audit` checks all transitive dependencies\n- **One version to track** \u2014 Jerboa's Cargo.lock pins the entire native dependency tree\n- **Smaller total size** \u2014 shared Rust runtime, LTO across all modules, dead code elimination\n\n---\n\n## C ABI Design\n\n### Principles\n\n1. **Every function is `extern \"C\"` with `#[no_mangle]`** \u2014 visible to Chez's `foreign-procedure`\n2. **Every function catches panics** \u2014 Rust panics across FFI are undefined behavior\n3. **Opaque handles for stateful objects** \u2014 TLS connections, database connections, regex patterns are opaque pointers\n4. **Error codes, not exceptions** \u2014 return `i32` status codes; error details via `jerboa_last_error()`\n5. **Caller-allocated buffers where possible** \u2014 avoid Rust allocating memory that Scheme must free\n6. **Consistent naming** \u2014 `jerboa_<module>_<operation>` prefix for all functions\n\n### Panic Safety\n\nEvery exported function wraps its body in `catch_unwind`:\n\n```rust\n// src/panic.rs\nuse std::panic;\n\n/// Thread-local error message for the last failed operation\nthread_local! {\n    static LAST_ERROR: std::cell::RefCell<String> = std::cell::RefCell::new(String::new());\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_last_error(buf: *mut u8, buf_len: usize) -> usize {\n    LAST_ERROR.with(|e| {\n        let msg = e.borrow();\n        let bytes = msg.as_bytes();\n        let copy_len = bytes.len().min(buf_len.saturating_sub(1));\n        if !buf.is_null() && copy_len > 0 {\n            unsafe {\n                std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, copy_len);\n                *buf.add(copy_len) = 0; // null terminate\n            }\n        }\n        bytes.len()\n    })\n}\n\npub fn ffi_wrap<F: FnOnce() -> i32 + panic::UnwindSafe>(f: F) -> i32 {\n    match panic::catch_unwind(f) {\n        Ok(code) => code,\n        Err(e) => {\n            let msg = if let Some(s) = e.downcast_ref::<&str>() {\n                s.to_string()\n            } else if let Some(s) = e.downcast_ref::<String>() {\n                s.clone()\n            } else {\n                \"unknown panic\".to_string()\n            };\n            LAST_ERROR.with(|cell| *cell.borrow_mut() = msg);\n            -1 // error sentinel\n        }\n    }\n}\n```\n\n### Example: Crypto Module\n\n```rust\n// src/crypto.rs\nuse ring::{digest, hmac, rand, aead, constant_time};\nuse crate::panic::ffi_wrap;\n\n// --- Digest ---\n\n#[no_mangle]\npub extern \"C\" fn jerboa_sha256(\n    input: *const u8, input_len: usize,\n    output: *mut u8, output_len: usize,\n) -> i32 {\n    ffi_wrap(|| {\n        if input.is_null() || output.is_null() || output_len < 32 {\n            return -1;\n        }\n        let data = unsafe { std::slice::from_raw_parts(input, input_len) };\n        let hash = digest::digest(&digest::SHA256, data);\n        unsafe {\n            std::ptr::copy_nonoverlapping(hash.as_ref().as_ptr(), output, 32);\n        }\n        0 // success\n    })\n}\n\n// --- CSPRNG ---\n\n#[no_mangle]\npub extern \"C\" fn jerboa_random_bytes(output: *mut u8, len: usize) -> i32 {\n    ffi_wrap(|| {\n        if output.is_null() {\n            return -1;\n        }\n        let rng = rand::SystemRandom::new();\n        let buf = unsafe { std::slice::from_raw_parts_mut(output, len) };\n        rand::SecureRandom::fill(&rng, buf).map(|_| 0).unwrap_or(-1)\n    })\n}\n\n// --- Constant-time comparison ---\n\n#[no_mangle]\npub extern \"C\" fn jerboa_timing_safe_equal(\n    a: *const u8, a_len: usize,\n    b: *const u8, b_len: usize,\n) -> i32 {\n    ffi_wrap(|| {\n        if a_len != b_len { return 0; }\n        let sa = unsafe { std::slice::from_raw_parts(a, a_len) };\n        let sb = unsafe { std::slice::from_raw_parts(b, b_len) };\n        if constant_time::verify_slices_are_equal(sa, sb).is_ok() { 1 } else { 0 }\n    })\n}\n\n// --- AEAD (AES-256-GCM) ---\n\n#[no_mangle]\npub extern \"C\" fn jerboa_aead_seal(\n    key: *const u8, key_len: usize,        // 32 bytes for AES-256-GCM\n    nonce: *const u8, nonce_len: usize,     // 12 bytes\n    plaintext: *const u8, pt_len: usize,\n    aad: *const u8, aad_len: usize,\n    output: *mut u8, output_max: usize,     // must be >= pt_len + 16 (tag)\n    output_len: *mut usize,\n) -> i32 {\n    ffi_wrap(|| {\n        // ... validate inputs, construct SealingKey, seal in place ...\n        0\n    })\n}\n```\n\n### Example: Regex Module\n\n```rust\n// src/regex.rs\nuse regex::Regex;\nuse crate::panic::ffi_wrap;\nuse std::collections::HashMap;\nuse std::sync::Mutex;\n\n// Opaque handle system\nstatic REGEX_STORE: Mutex<HashMap<u64, Regex>> = Mutex::new(HashMap::new());\nstatic NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);\n\n#[no_mangle]\npub extern \"C\" fn jerboa_regex_compile(\n    pattern: *const u8, pattern_len: usize,\n    handle: *mut u64,\n) -> i32 {\n    ffi_wrap(|| {\n        let pat = unsafe { std::str::from_utf8_unchecked(\n            std::slice::from_raw_parts(pattern, pattern_len)) };\n        match Regex::new(pat) {\n            Ok(re) => {\n                let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst);\n                REGEX_STORE.lock().unwrap().insert(id, re);\n                unsafe { *handle = id; }\n                0\n            }\n            Err(_) => -1,\n        }\n    })\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_regex_is_match(\n    handle: u64,\n    text: *const u8, text_len: usize,\n) -> i32 {\n    ffi_wrap(|| {\n        let store = REGEX_STORE.lock().unwrap();\n        let re = match store.get(&handle) {\n            Some(r) => r,\n            None => return -1,\n        };\n        let s = unsafe { std::str::from_utf8_unchecked(\n            std::slice::from_raw_parts(text, text_len)) };\n        if re.is_match(s) { 1 } else { 0 }\n    })\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_regex_free(handle: u64) -> i32 {\n    ffi_wrap(|| {\n        REGEX_STORE.lock().unwrap().remove(&handle);\n        0\n    })\n}\n```\n\n---\n\n## FFI Binding Patterns\n\n### Jerboa Side\n\n```scheme\n;; lib/std/native.sls \u2014 load the unified Rust library\n(library (std native)\n  (export load-jerboa-native)\n  (import (chezscheme))\n\n  (define (load-jerboa-native)\n    (load-shared-object \"libjerboa_native.so\")))\n```\n\n```scheme\n;; lib/std/crypto/native.sls \u2014 crypto bindings\n(library (std crypto native)\n  (export sha256 random-bytes timing-safe-equal? aead-seal aead-open)\n  (import (chezscheme) (std native))\n\n  (load-jerboa-native)\n\n  (define jerboa-sha256\n    (foreign-procedure \"jerboa_sha256\"\n      (u8* size_t u8* size_t) int))\n\n  (define (sha256 bv)\n    (let ([out (make-bytevector 32)])\n      (let ([rc (jerboa-sha256 bv (bytevector-length bv) out 32)])\n        (when (< rc 0)\n          (error 'sha256 \"hash failed\"))\n        out)))\n\n  (define jerboa-random-bytes\n    (foreign-procedure \"jerboa_random_bytes\" (u8* size_t) int))\n\n  (define (random-bytes n)\n    (let ([bv (make-bytevector n)])\n      (let ([rc (jerboa-random-bytes bv n)])\n        (when (< rc 0)\n          (error 'random-bytes \"CSPRNG failed\"))\n        bv)))\n\n  (define jerboa-timing-safe-equal\n    (foreign-procedure \"jerboa_timing_safe_equal\"\n      (u8* size_t u8* size_t) int))\n\n  (define (timing-safe-equal? a b)\n    (= 1 (jerboa-timing-safe-equal a (bytevector-length a)\n                                    b (bytevector-length b))))\n\n  ;; ... aead-seal, aead-open similarly\n  )\n```\n\n```scheme\n;; lib/std/pcre2.sls replacement \u2014 now backed by Rust regex\n(library (std regex-native)\n  (export regex-compile regex-match? regex-find regex-free)\n  (import (chezscheme) (std native))\n\n  (load-jerboa-native)\n\n  (define jerboa-regex-compile\n    (foreign-procedure \"jerboa_regex_compile\" (u8* size_t void*) int))\n\n  (define jerboa-regex-is-match\n    (foreign-procedure \"jerboa_regex_is_match\" (unsigned-64 u8* size_t) int))\n\n  (define jerboa-regex-free\n    (foreign-procedure \"jerboa_regex_free\" (unsigned-64) int))\n\n  (define (regex-compile pattern)\n    (let ([bv (string->utf8 pattern)]\n          [handle-box (make-bytevector 8)])\n      (let ([rc (jerboa-regex-compile bv (bytevector-length bv) handle-box)])\n        (when (< rc 0)\n          (error 'regex-compile \"invalid pattern\" pattern))\n        (bytevector-u64-native-ref handle-box 0))))\n\n  (define (regex-match? handle text)\n    (let ([bv (string->utf8 text)])\n      (= 1 (jerboa-regex-is-match handle bv (bytevector-length bv)))))\n\n  (define (regex-free handle)\n    (jerboa-regex-free handle))\n  )\n```\n\n---\n\n## Secure Memory Region\n\nThe secure region allocator from `vs-rust.md`, implemented in Rust:\n\n```rust\n// src/secure_mem.rs\nuse libc::{mmap, munmap, mlock, munlock, madvise, mprotect};\nuse libc::{MAP_PRIVATE, MAP_ANONYMOUS, PROT_READ, PROT_WRITE, PROT_NONE};\nuse libc::{MADV_DONTDUMP, MADV_DONTFORK};\nuse std::ptr;\nuse crate::panic::ffi_wrap;\n\nconst GUARD_PAGE_SIZE: usize = 4096;\n\n#[no_mangle]\npub extern \"C\" fn jerboa_secure_alloc(size: usize) -> *mut u8 {\n    ffi_wrap_ptr(|| {\n        // Allocate: guard page + data + guard page\n        let total = GUARD_PAGE_SIZE + size + GUARD_PAGE_SIZE;\n        let base = unsafe {\n            mmap(ptr::null_mut(), total, PROT_READ | PROT_WRITE,\n                 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)\n        };\n        if base == libc::MAP_FAILED { return ptr::null_mut(); }\n\n        // Protect guard pages (PROT_NONE \u2014 any access = SIGSEGV)\n        unsafe {\n            mprotect(base, GUARD_PAGE_SIZE, PROT_NONE);\n            mprotect(base.add(GUARD_PAGE_SIZE + size), GUARD_PAGE_SIZE, PROT_NONE);\n        }\n\n        let data = unsafe { base.add(GUARD_PAGE_SIZE) as *mut u8 };\n\n        // Lock into RAM \u2014 never swapped to disk\n        unsafe { mlock(data as *mut _, size); }\n\n        // Exclude from core dumps\n        unsafe { madvise(data as *mut _, size, MADV_DONTDUMP); }\n\n        // Don't inherit in child processes\n        unsafe { madvise(data as *mut _, size, MADV_DONTFORK); }\n\n        data\n    })\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_secure_free(ptr: *mut u8, size: usize) -> i32 {\n    ffi_wrap(|| {\n        if ptr.is_null() { return -1; }\n\n        // Wipe \u2014 explicit_bzero is guaranteed not to be optimized away\n        unsafe { libc::explicit_bzero(ptr as *mut _, size); }\n\n        // Unlock\n        unsafe { munlock(ptr as *mut _, size); }\n\n        // Unmap entire region including guard pages\n        let base = unsafe { ptr.sub(GUARD_PAGE_SIZE) };\n        let total = GUARD_PAGE_SIZE + size + GUARD_PAGE_SIZE;\n        unsafe { munmap(base as *mut _, total); }\n\n        0\n    })\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_secure_wipe(ptr: *mut u8, size: usize) -> i32 {\n    ffi_wrap(|| {\n        if ptr.is_null() { return -1; }\n        unsafe { libc::explicit_bzero(ptr as *mut _, size); }\n        0\n    })\n}\n\n#[no_mangle]\npub extern \"C\" fn jerboa_secure_random_fill(ptr: *mut u8, size: usize) -> i32 {\n    ffi_wrap(|| {\n        if ptr.is_null() { return -1; }\n        let rng = ring::rand::SystemRandom::new();\n        let buf = unsafe { std::slice::from_raw_parts_mut(ptr, size) };\n        ring::rand::SecureRandom::fill(&rng, buf).map(|_| 0).unwrap_or(-1)\n    })\n}\n\n// Helper: ffi_wrap for pointer-returning functions\nfn ffi_wrap_ptr<F: FnOnce() -> *mut u8 + std::panic::UnwindSafe>(f: F) -> *mut u8 {\n    match std::panic::catch_unwind(f) {\n        Ok(ptr) => ptr,\n        Err(_) => ptr::null_mut(),\n    }\n}\n```\n\nJerboa side:\n\n```scheme\n;; lib/std/crypto/secure-mem.sls\n(library (std crypto secure-mem)\n  (export with-secure-region secure-alloc secure-free secure-wipe)\n  (import (chezscheme) (std native))\n\n  (load-jerboa-native)\n\n  (define jerboa-secure-alloc\n    (foreign-procedure \"jerboa_secure_alloc\" (size_t) void*))\n  (define jerboa-secure-free\n    (foreign-procedure \"jerboa_secure_free\" (void* size_t) int))\n  (define jerboa-secure-wipe\n    (foreign-procedure \"jerboa_secure_wipe\" (void* size_t) int))\n  (define jerboa-secure-random-fill\n    (foreign-procedure \"jerboa_secure_random_fill\" (void* size_t) int))\n\n  (define (secure-alloc size)\n    (let ([ptr (jerboa-secure-alloc size)])\n      (when (zero? (ftype-pointer-address ptr))\n        (error 'secure-alloc \"mmap failed\" size))\n      (cons ptr size)))  ;; pair of (pointer . size) for tracking\n\n  (define (secure-free region)\n    (jerboa-secure-free (car region) (cdr region)))\n\n  (define-syntax with-secure-region\n    (syntax-rules ()\n      [(_ ([name size] ...) body ...)\n       (let ([name (secure-alloc size)] ...)\n         (dynamic-wind\n           void\n           (lambda () body ...)\n           (lambda ()\n             (secure-free name) ...)))]))\n  )\n```\n\n---\n\n## What Each Swap Fixes\n\n### Vulnerabilities from security.md\n\n| Vulnerability | Fixed By | How |\n|---------------|----------|-----|\n| **V1**: Command injection in digest | ring (crypto.rs) | No shell, no temp files \u2014 direct hash computation |\n| **V3**: Predictable nonces | ring CSPRNG | `SystemRandom` uses OS entropy |\n| **V5**: Weak actor auth (FNV-1a) | ring HMAC | HMAC-SHA256 with proper key derivation |\n| **V8**: WebSocket handshake stub | ring SHA-1 | Proper `SHA-1(key + GUID)` computation |\n| **C1-C6**: Crypto foundation | ring + secure_mem | Complete crypto suite with secure memory |\n\n### Bug classes from security2.md\n\n| Bug Class | Fixed By | How |\n|-----------|----------|-----|\n| **ReDoS** | Rust regex crate | NFA engine \u2014 linear time guaranteed, backtracking impossible |\n| **Zlib decompression bombs** | flate2 (compress.rs) | Output size limit parameter in Rust wrapper |\n| ~~**YAML C parser bugs**~~ | ~~unsafe-libyaml~~ | **Eliminated** \u2014 `(std text yaml)` is now pure Scheme, no C/Rust needed |\n| **FFI null pointer deref** | Rust wrappers | Every function validates inputs and checks returns |\n| **FFI type confusion** | Rust type system | Rust compiler rejects type mismatches |\n| **Secret material in GC heap** | secure_mem.rs | mlock'd region outside GC, wiped on free |\n\n### What's NOT fixed by Rust swap alone\n\nThese still need the Scheme-side fixes from security2.md:\n\n| Bug Class | Why Rust Doesn't Help |\n|-----------|----------------------|\n| Parser recursion depth (reader, JSON) | Bug is in Scheme code, not C libraries |\n| Bytevector bounds checks (HTTP/2, WebSocket, DNS) | Bug is in Scheme code, not C libraries |\n| Silent data corruption (base64, CSV, hex) | Bug is in Scheme code, not C libraries |\n| Format string injection | Bug is in Scheme code |\n| Sandbox escape vectors | Bug is in Scheme code |\n| Capability forgery (V2) | Bug is in Scheme code |\n\nThe Rust native library and the security2.md Scheme fixes are complementary \u2014 neither is a substitute for the other.\n\n---\n\n## Build Integration\n\n### Makefile Additions\n\n```makefile\n# Rust native library\nRUST_NATIVE_DIR = jerboa-native-rs\nRUST_NATIVE_LIB = $(RUST_NATIVE_DIR)/target/release/libjerboa_native.so\n\n$(RUST_NATIVE_LIB): $(RUST_NATIVE_DIR)/src/*.rs $(RUST_NATIVE_DIR)/Cargo.toml\n\tcd $(RUST_NATIVE_DIR) && cargo build --release\n\nnative: $(RUST_NATIVE_LIB)\n\tcp $(RUST_NATIVE_LIB) lib/\n\nclean-native:\n\tcd $(RUST_NATIVE_DIR) && cargo clean\n\n# Run tests with native library available\ntest-native: native\n\tLD_LIBRARY_PATH=lib $(SCHEME) --libdirs lib --script tests/test-native.ss\n\n# Audit dependencies for known vulnerabilities\naudit-native:\n\tcd $(RUST_NATIVE_DIR) && cargo audit\n\n# Generate C header (for documentation / external consumers)\nheader-native:\n\tcd $(RUST_NATIVE_DIR) && cbindgen --output include/jerboa_native.h\n```\n\n### Static Linking for Single-Binary Deployment\n\nFor `jerboa/build.sls` static binary builds, link the Rust library statically:\n\n```toml\n# Cargo.toml \u2014 also produce a static lib\n[lib]\ncrate-type = [\"cdylib\", \"staticlib\"]\n```\n\n```makefile\n# Static build: link libjerboa_native.a into the Chez binary\nRUST_STATIC_LIB = $(RUST_NATIVE_DIR)/target/release/libjerboa_native.a\n\nstatic-binary: $(RUST_STATIC_LIB)\n\t$(SCHEME) --libdirs lib --script build-static.ss \\\n\t  --extra-libs \"$(RUST_STATIC_LIB) -lpthread -ldl -lm\"\n```\n\nThe result is a single binary with zero runtime dependencies beyond libc \u2014 Chez runtime + Jerboa libraries + Rust native code, all in one ELF file.\n\n---\n\n## Migration Strategy\n\n### Current Status\n\nWe are in **Phase 1** (parallel installation). The Rust implementations are complete for crypto, compression, regex, databases (SQLite, PostgreSQL), and OS integration (epoll, inotify, landlock). However, the \"default\" modules (e.g., `(std db sqlite)`, `(std crypto cipher)`) still import from chez-* C libraries. The Rust-backed modules are available as separate imports (e.g., `(std db sqlite-native)`, `(std crypto native-rust)`).\n\n**TLS is deferred** \u2014 `(std net ssl)` still requires chez-ssl / OpenSSL. The rustls-ffi integration is planned but not yet implemented due to the complexity of stateful TLS session management.\n\n### Phase 1: Parallel Installation (CURRENT)\n\nBoth C libraries and the Rust library are available as separate modules. New code should prefer the `-native` / `native-rust` modules:\n\n```scheme\n;; Preferred \u2014 uses Rust ring via libjerboa_native.so\n(import (std crypto native-rust))\n\n;; Legacy \u2014 uses OpenSSL via chez-crypto\n(import (std crypto cipher))\n```\n\nThis allows incremental testing \u2014 run the full test suite against both backends and compare results.\n\n### Phase 2: Native-First\n\nThe Rust library becomes the default import for all modules that have Rust replacements. The chez-* modules become explicitly legacy.\n\n### Phase 3: C Removal\n\nRemove the chez-* wrapper dependencies entirely. The Rust library is the only native code Jerboa links (besides Chez Scheme itself and libc).\n\n---\n\n## Implementation Roadmap\n\n### Week 1-2: Core \u2014 Crypto + Secure Memory \u2705 IMPLEMENTED\n\n| Task | Crate | Functions | Status |\n|------|-------|-----------|--------|\n| Digests (SHA-1, SHA-256, SHA-384, SHA-512) | ring | `jerboa_sha1`, `jerboa_sha256`, `jerboa_sha384`, `jerboa_sha512` | \u2705 |\n| HMAC | ring | `jerboa_hmac_sha256`, `jerboa_hmac_sha256_verify` | \u2705 |\n| CSPRNG | ring | `jerboa_random_bytes` | \u2705 |\n| Constant-time compare | ring | `jerboa_timing_safe_equal` | \u2705 |\n| AEAD (AES-256-GCM) | ring | `jerboa_aead_seal`, `jerboa_aead_open` | \u2705 |\n| PBKDF2 | ring | `jerboa_pbkdf2_derive`, `jerboa_pbkdf2_verify` | \u2705 |\n| Secure memory region | libc (mmap/mlock) | `jerboa_secure_alloc`, `jerboa_secure_free`, `jerboa_secure_wipe` | \u2705 |\n| Scheme bindings | \u2014 | `(std crypto native-rust)`, `(std crypto secure-mem)` | \u2705 |\n\nRust: `src/crypto.rs`, `src/secure_mem.rs`. Scheme: `lib/std/crypto/native-rust.sls`, `lib/std/crypto/secure-mem.sls`. Tests: `tests/test-native-rust.ss` (31 tests).\n\n### Week 3: TLS \u2014 DEFERRED\n\n| Task | Crate | Functions | Status |\n|------|-------|-----------|--------|\n| TLS client | rustls + rustls-ffi | `jerboa_tls_connect`, `jerboa_tls_read`, `jerboa_tls_write`, `jerboa_tls_close` | \u23f3 |\n| TLS server | rustls + rustls-ffi | `jerboa_tls_accept` | \u23f3 |\n| Certificate loading | rustls-pemfile | `jerboa_tls_load_cert`, `jerboa_tls_load_key` | \u23f3 |\n\nTLS requires stateful session management with complex async I/O. Deferred to a future phase.\n\n### Week 4: Regex + Compression \u2705 IMPLEMENTED\n\n| Task | Crate | Functions | Status |\n|------|-------|-----------|--------|\n| Regex compile/match/find/replace | regex (NFA) | `jerboa_regex_compile`, `jerboa_regex_is_match`, `jerboa_regex_find`, `jerboa_regex_replace_all`, `jerboa_regex_free` | \u2705 |\n| Deflate/inflate with size limits | flate2 | `jerboa_deflate`, `jerboa_inflate` | \u2705 |\n| Gzip/gunzip with size limits | flate2 | `jerboa_gzip`, `jerboa_gunzip` | \u2705 |\n| Scheme bindings | \u2014 | `(std regex-native)`, `(std compress native-rust)` | \u2705 |\n\nRust: `src/regex_native.rs`, `src/compress.rs`. Scheme: `lib/std/regex-native.sls`, `lib/std/compress/native-rust.sls`. Tests: `tests/test-native-rust.ss`.\n\n### Week 5: Databases \u2705 IMPLEMENTED\n\n| Task | Crate | Functions | Status |\n|------|-------|-----------|--------|\n| SQLite open/close/exec | rusqlite (bundled) | `jerboa_sqlite_open`, `jerboa_sqlite_close`, `jerboa_sqlite_exec` | \u2705 |\n| SQLite prepare/bind/step/column | rusqlite | `jerboa_sqlite_prepare`, `jerboa_sqlite_bind_*`, `jerboa_sqlite_step`, `jerboa_sqlite_column_*` | \u2705 |\n| SQLite reset/finalize/metadata | rusqlite | `jerboa_sqlite_reset`, `jerboa_sqlite_finalize`, `jerboa_sqlite_last_insert_rowid`, `jerboa_sqlite_changes` | \u2705 |\n| PostgreSQL connect/disconnect | rust-postgres | `jerboa_pg_connect`, `jerboa_pg_disconnect` | \u2705 |\n| PostgreSQL exec/query | rust-postgres | `jerboa_pg_exec`, `jerboa_pg_query`, `jerboa_pg_nrows`, `jerboa_pg_ncols` | \u2705 |\n| PostgreSQL result access | rust-postgres | `jerboa_pg_get_value`, `jerboa_pg_is_null`, `jerboa_pg_column_name`, `jerboa_pg_free_result` | \u2705 |\n| Scheme bindings | \u2014 | `(std db sqlite-native)`, `(std db postgresql-native)` | \u2705 |\n\nRust: `src/sqlite.rs`, `src/postgres_native.rs`. Scheme: `lib/std/db/sqlite-native.sls`, `lib/std/db/postgresql-native.sls`. Tests: `tests/test-native-rust-week5-6.ss` (26 tests). SQLite uses raw `sqlite3_*` C API via `rusqlite::ffi` to avoid Rust lifetime issues with Statement handles. PostgreSQL uses handle stores for `Client` and `Vec<Row>`.\n\n### Week 6: OS Integration \u2705 IMPLEMENTED\n\n| Task | Crate | Functions | Status |\n|------|-------|-----------|--------|\n| epoll create/ctl/wait/close | libc | `jerboa_epoll_create`, `jerboa_epoll_ctl`, `jerboa_epoll_wait`, `jerboa_epoll_close` | \u2705 |\n| inotify init/watch/read/close | libc | `jerboa_inotify_init`, `jerboa_inotify_add_watch`, `jerboa_inotify_rm_watch`, `jerboa_inotify_read`, `jerboa_inotify_close` | \u2705 |\n| Landlock ABI/ruleset/rules/enforce | libc (syscalls) | `jerboa_landlock_abi_version`, `jerboa_landlock_create_ruleset`, `jerboa_landlock_add_path_rule`, `jerboa_landlock_add_net_rule`, `jerboa_landlock_enforce` | \u2705 |\n| io_uring | io-uring | \u2014 | \u23f3 Deferred |\n| Scheme bindings | \u2014 | `(std os epoll-native)`, `(std os inotify-native)`, `(std os landlock-native)` | \u2705 |\n\nRust: `src/epoll.rs`, `src/inotify_native.rs`, `src/landlock.rs`. Scheme: `lib/std/os/epoll-native.sls`, `lib/std/os/inotify-native.sls`, `lib/std/os/landlock-native.sls`. Tests: `tests/test-native-rust-week5-6.ss`. Landlock supports ABI v1-v7 with filesystem + network rules. io_uring deferred (complex async interface).\n\n---\n\n## Summary\n\n| Metric | Before (C libraries) | After (Rust native) |\n|--------|---------------------|---------------------|\n| Shared libraries loaded | 10+ | 1 |\n| C code in trust boundary | ~500K lines (OpenSSL + SQLite + ...; libyaml already eliminated via pure Scheme) | 0 (Rust only; C primitives inside ring are audited asm) |\n| Memory safety CVEs possible | Yes \u2014 every C library | No \u2014 Rust compiler prevents them |\n| ReDoS possible | Yes \u2014 PCRE2 backtracks | No \u2014 Rust regex uses NFA |\n| Secret wiping guarantee | No \u2014 GC copies | Yes \u2014 secure region is outside GC |\n| Dependency audit | Manual per-library | `cargo audit` \u2014 automated |\n| Build reproducibility | Varies per library | `Cargo.lock` \u2014 exact pins |\n| Static binary size impact | ~15-25MB (all C libs) | ~5-10MB (LTO, dead code eliminated) |\n"}
 {"text": "<!-- FILE: docs/stm.md -->\n# Software Transactional Memory in Jerboa\n\n`(std stm)` provides optimistic concurrency through transactional variables\n(TVars). Transactions execute speculatively, validate their read-set at\ncommit time, and automatically retry on conflict \u2014 all without requiring the\nprogrammer to acquire or release locks.\n\n---\n\n## Overview\n\nTraditional lock-based concurrency is error-prone: locks must be acquired in\na consistent order to avoid deadlock, held for the right duration to avoid\nraces, and released even when exceptions occur. STM eliminates these problems\nby treating shared state mutations as database transactions:\n\n1. A thread **reads** TVars freely during a transaction. First reads are\n   timestamped.\n2. Writes are buffered in a **write-set** private to the thread.\n3. At the end of `atomically`, the runtime acquires the **global commit\n   mutex** and validates that every TVar read during the transaction still\n   has the same version number it had at the time of the first read.\n4. If valid, writes are applied atomically and version counters are\n   incremented. A broadcast on the commit condition variable wakes any\n   threads blocked in `retry`.\n5. If any version has changed (conflict), the entire transaction is discarded\n   and re-run from the beginning.\n\n**Key properties:**\n\n- **Composable**: two atomic operations can be composed into a larger atomic\n  operation simply by nesting or sequencing inside `atomically`.\n- **Deadlock-free**: there are no locks for application code to acquire; only\n  the internal commit mutex is used, and it is held only for the validation\n  and write phase.\n- **No explicit cleanup**: exceptions inside a transaction leave no partial\n  state behind. The write-set is simply discarded.\n\n---\n\n## Importing the Library\n\n```scheme\n(import (std stm))\n```\n\n---\n\n## Core API\n\n### `make-tvar` \u2014 create a transactional variable\n\n```scheme\n(make-tvar init) => tvar\n```\n\nCreates a new TVar holding `init` as its initial committed value with version\n`0`.\n\n```scheme\n(define balance (make-tvar 1000))\n(define inventory (make-tvar '()))\n```\n\n---\n\n### `tvar?` \u2014 predicate\n\n```scheme\n(tvar? x) => boolean\n```\n\nReturns `#t` if `x` is a TVar.\n\n---\n\n### `tvar-ref` \u2014 read outside a transaction\n\n```scheme\n(tvar-ref tv) => value\n```\n\nReads the current committed value of `tv` without starting a transaction.\nUseful for observation and debugging. **Do not use inside `atomically`** \u2014\nuse `tvar-read` instead, or the read will not be recorded in the\ntransaction's read-set and conflicts will go undetected.\n\n```scheme\n(display (tvar-ref balance))   ; safe outside transactions\n```\n\n---\n\n### `atomically` \u2014 run a transaction\n\n```scheme\n(atomically body ...) => value\n```\n\nEvaluates `body ...` as a single atomic transaction and returns the value of\nthe last expression. The body runs speculatively: if the transaction\nconflicts with a concurrent commit it is re-run automatically. If the body\ncalls `retry`, the thread blocks until at least one TVar in its read-set is\nmodified by another transaction, then re-runs.\n\n**Nesting**: if `atomically` is called while already inside a transaction the\ninner call flattens into the enclosing transaction \u2014 all reads and writes\nparticipate in the same commit.\n\n```scheme\n(atomically\n  (let ([bal (tvar-read balance)])\n    (tvar-write! balance (- bal amount))))\n```\n\n---\n\n### `tvar-read` \u2014 transactional read\n\n```scheme\n(tvar-read tv) => value\n```\n\nReads `tv` inside a transaction. On the first read of a TVar the current\nversion is snapshotted into the transaction's read-set. Subsequent reads of\nthe same TVar return the current committed value (consistency is enforced at\ncommit time, not per-read). If the TVar has been written in the current\ntransaction, the buffered value is returned instead.\n\nCalled outside a transaction, `tvar-read` behaves like `tvar-ref` (direct\nread, no recording).\n\n---\n\n### `tvar-write!` \u2014 transactional write\n\n```scheme\n(tvar-write! tv val)\n```\n\nBuffers a write to `tv` in the current transaction's write-set. The actual\ncommitted value is not changed until the transaction commits successfully.\nMultiple writes to the same TVar within one transaction are coalesced: only\nthe last write is applied.\n\nCalled outside a transaction, `tvar-write!` writes directly with the commit\nmutex held and broadcasts on the condition variable.\n\n---\n\n### `retry` \u2014 block until something changes\n\n```scheme\n(retry)\n```\n\nAborts the current transaction and blocks the calling thread until at least\none TVar that was read during this transaction is modified by another\ncommitted transaction. The thread then re-runs the transaction from the\nbeginning.\n\n`retry` is the STM equivalent of a condition wait, but it is entirely\ndeclarative: you express _when_ to retry (a guard condition is not yet\nsatisfied) and the runtime figures out _what_ to wait for (the TVars whose\nstate determines the condition).\n\n```scheme\n(define (dequeue! q)\n  (atomically\n    (let ([items (tvar-read q)])\n      (if (null? items)\n        (retry)                          ; block until queue is non-empty\n        (begin\n          (tvar-write! q (cdr items))\n          (car items))))))\n```\n\n---\n\n### `or-else` \u2014 alternative on retry\n\n```scheme\n(or-else expr1 expr2)\n```\n\nEvaluates `expr1`. If `expr1` calls `retry`, evaluates `expr2` instead.\nBoth expressions run within the same enclosing transaction if one is active.\n`or-else` allows you to try multiple strategies in priority order, falling\nback gracefully when none is immediately satisfiable.\n\n```scheme\n(define (dequeue-any! high-priority-q low-priority-q)\n  (atomically\n    (or-else\n      (dequeue-inner! high-priority-q)   ; try high-priority first\n      (dequeue-inner! low-priority-q)))) ; fall back to low-priority\n```\n\n---\n\n## How It Works Internally\n\n### TVar structure\n\nEach TVar holds:\n- `stm-value`: the current committed value\n- `stm-version`: a monotonically increasing integer, incremented on every\n  successful write\n\n### Transaction record\n\nEach transaction maintains:\n- `tx-read-set`: association list of `(tvar . version-at-first-read)`\n- `tx-write-set`: association list of `(tvar . new-value)`\n\nThe read-set records the version seen the first time each TVar is read. The\nwrite-set buffers pending writes.\n\n### Commit protocol\n\n1. Acquire `*commit-mutex*` (single global mutex).\n2. Walk the read-set. For each entry, compare the TVar's current version\n   against the snapshotted version. If any has changed: **conflict** \u2014\n   release mutex, discard state, re-run.\n3. If all versions match: apply the write-set. For each written TVar,\n   update `stm-value` and increment `stm-version`.\n4. If any writes were applied, broadcast on `*commit-cond*` to wake threads\n   blocked in `retry`.\n5. Release the mutex.\n\n### Retry blocking\n\nWhen `retry` is called:\n1. The `&stm-retry` condition is raised.\n2. `atomically` catches it, acquires the commit mutex, and calls\n   `condition-wait` on `*commit-cond*`.\n3. When any transaction commits with writes, `condition-broadcast` wakes all\n   waiting threads.\n4. The thread re-acquires the mutex, releases it, and re-runs the entire\n   transaction.\n\nThis is a coarse wake-up: all threads sleeping in `retry` wake when _any_\nTVar changes, even one they did not read. The transaction's own guard will\nsimply call `retry` again if the relevant TVar still has an unsatisfactory\nvalue, and the thread will sleep again.\n\n---\n\n## Complete Examples\n\n### Bank account transfer\n\nA classic problem: transfer funds between two accounts atomically. Without\nSTM this requires careful lock ordering. With STM you just read and write.\n\n```scheme\n(import (chezscheme) (std stm))\n\n(define account-a (make-tvar 500))\n(define account-b (make-tvar 300))\n\n(define (transfer! from to amount)\n  (atomically\n    (let ([from-bal (tvar-read from)])\n      (when (< from-bal amount)\n        (error 'transfer! \"insufficient funds\" from-bal amount))\n      (tvar-write! from (- from-bal amount))\n      (tvar-write! to   (+ (tvar-read to) amount)))))\n\n(transfer! account-a account-b 200)\n(display (tvar-ref account-a))   ; => 300\n(display (tvar-ref account-b))   ; => 500\n```\n\nThe read of `from` and the read of `to` are both in the same transaction's\nread-set. If another thread modifies either account concurrently, the commit\nwill fail and the entire transfer re-runs \u2014 the accounts are always\nconsistent.\n\n---\n\n### Bounded queue with STM\n\nA thread-safe bounded queue where producers block when full and consumers\nblock when empty.\n\n```scheme\n(import (chezscheme) (std stm))\n\n(define (make-stm-queue capacity)\n  (cons (make-tvar '())         ; items (as a list)\n        (make-tvar capacity)))  ; remaining capacity\n\n(define (queue-push! q item)\n  (let ([items-tv (car q)]\n        [cap-tv   (cdr q)])\n    (atomically\n      (let ([cap (tvar-read cap-tv)])\n        (when (fxzero? cap)\n          (retry))                           ; block until space is available\n        (tvar-write! items-tv (append (tvar-read items-tv) (list item)))\n        (tvar-write! cap-tv   (fx- cap 1))))))\n\n(define (queue-pop! q)\n  (let ([items-tv (car q)]\n        [cap-tv   (cdr q)])\n    (atomically\n      (let ([items (tvar-read items-tv)])\n        (when (null? items)\n          (retry))                           ; block until item is available\n        (tvar-write! items-tv (cdr items))\n        (tvar-write! cap-tv   (fx+ (tvar-read cap-tv) 1))\n        (car items)))))\n\n;; Usage\n(define q (make-stm-queue 10))\n\n(fork-thread (lambda ()\n  (let loop ([i 0])\n    (queue-push! q i)\n    (loop (+ i 1)))))\n\n(fork-thread (lambda ()\n  (let loop ()\n    (display (queue-pop! q))\n    (loop))))\n```\n\n---\n\n### Dining philosophers without deadlock\n\nThe classic dining philosophers problem has no deadlock with STM because\nthere are no locks to acquire in order.\n\n```scheme\n(import (chezscheme) (std stm))\n\n;; Five forks; each TVar holds #t = in use, #f = available\n(define fork-count 5)\n(define forks-tv (map make-tvar (make-list fork-count #f)))\n\n(define (pick-up-forks! i)\n  (let ([left  (list-ref forks-tv i)]\n        [right (list-ref forks-tv (modulo (+ i 1) fork-count))])\n    (atomically\n      (when (or (tvar-read left) (tvar-read right))\n        (retry))                   ; both must be available simultaneously\n      (tvar-write! left  #t)\n      (tvar-write! right #t))))\n\n(define (put-down-forks! i)\n  (let ([left  (list-ref forks-tv i)]\n        [right (list-ref forks-tv (modulo (+ i 1) fork-count))])\n    (atomically\n      (tvar-write! left  #f)\n      (tvar-write! right #f))))\n\n(define (philosopher i)\n  (let loop ()\n    (pick-up-forks! i)\n    (display (string-append \"Philosopher \" (number->string i) \" eating\\n\"))\n    (put-down-forks! i)\n    (loop)))\n\n(for-each (lambda (i) (fork-thread (lambda () (philosopher i))))\n          '(0 1 2 3 4))\n```\n\nNo deadlock is possible: `retry` inside `pick-up-forks!` blocks until\n_both_ forks are simultaneously available, and the runtime handles the\nwaiting without any locks in application code.\n\n---\n\n### Priority scheduling with `or-else`\n\n```scheme\n(import (chezscheme) (std stm))\n\n(define urgent-queue  (make-tvar '()))\n(define normal-queue  (make-tvar '()))\n\n(define (enqueue! tv item)\n  (atomically (tvar-write! tv (append (tvar-read tv) (list item)))))\n\n(define (dequeue-inner! tv)\n  ;; Must be called inside an enclosing atomically\n  (let ([items (tvar-read tv)])\n    (if (null? items)\n      (retry)\n      (begin (tvar-write! tv (cdr items)) (car items)))))\n\n(define (next-task!)\n  (atomically\n    (or-else\n      (dequeue-inner! urgent-queue)   ; always prefer urgent work\n      (dequeue-inner! normal-queue))))\n\n;; If urgent-queue is empty, dequeue-inner! calls retry and or-else\n;; falls through to normal-queue. If both are empty, the whole\n;; next-task! call blocks until either queue has an item.\n```\n\n---\n\n## Performance\n\n### When STM shines\n\n- **Low contention**: when threads rarely conflict, transactions commit on\n  the first attempt and the overhead is one read-set validation pass plus a\n  mutex acquisition. For workloads with few writers and many readers this\n  is very efficient.\n- **Composability**: combining two independently correct atomic operations\n  into a larger atomic operation requires no redesign. With locks you would\n  need to expose and coordinate the internal locks, risking deadlock.\n- **Complex multi-variable invariants**: maintaining invariants across\n  several TVars is natural \u2014 either all writes commit or none do.\n\n### When to prefer traditional locking\n\n- **High contention with large write-sets**: when many threads frequently\n  write to the same TVars, conflict rates rise and transactions must re-run\n  repeatedly. A dedicated mutex or channel may perform better.\n- **Very tight inner loops**: the read-set allocation (an assoc list) adds\n  GC pressure. For sub-microsecond hot paths, fixnum-only `mutex` sections\n  may be faster.\n- **Write-heavy single-variable updates**: if you are just atomically\n  incrementing a counter, a `mutex`-guarded increment is simpler and\n  marginally faster than a full transaction.\n\n---\n\n## Pitfalls\n\n### Side effects inside transactions\n\nTransactions may run **more than once**. Any side effect in the body \u2014\nprinting, writing to a file, sending a network packet \u2014 will be repeated on\neach retry. Place side effects after `atomically` returns, not inside it.\n\n```scheme\n;; WRONG: display may run multiple times\n(atomically\n  (display \"withdrawing...\")      ; runs on every retry\n  (tvar-write! balance (- (tvar-read balance) amount)))\n\n;; CORRECT: side effect after the transaction\n(atomically\n  (tvar-write! balance (- (tvar-read balance) amount)))\n(display \"withdrawal complete\")\n```\n\n### `tvar-ref` inside transactions\n\nUsing `tvar-ref` inside `atomically` bypasses the read-set recording. The\nvalue is read from committed state but the version is not snapshotted, so\nconflicting concurrent writes will not be detected at commit time. Always use\n`tvar-read` inside transactions.\n\n```scheme\n;; WRONG: conflict on balance will not be caught\n(atomically\n  (let ([bal (tvar-ref balance)])   ; not recorded in read-set!\n    (tvar-write! balance (- bal 10))))\n\n;; CORRECT\n(atomically\n  (let ([bal (tvar-read balance)])\n    (tvar-write! balance (- bal 10))))\n```\n\n### Infinite retry loops\n\nIf the condition guarded by `retry` can never become true, the thread will\nblock forever. Ensure that some other thread can plausibly modify the TVars\nin the read-set and satisfy the guard.\n\n### Coarse-grained wakeup\n\nAll threads sleeping in `retry` are woken whenever any transaction commits\nwith writes, not just those waiting for the specific TVars they read.\nHigh-contention systems with many `retry`-blocked threads may see spurious\nwakeups. Each spurious wakeup results in a transaction re-run that calls\n`retry` again, so correctness is not affected \u2014 only efficiency. For very\nlarge numbers of blocked threads, consider partitioning TVars into smaller\ngroups or using more targeted condition variables.\n\n### `or-else` and write-set accumulation\n\nBoth branches of `or-else` run within the same enclosing transaction if\none exists. Writes buffered by a branch that subsequently calls `retry` are\n_not_ rolled back before the alternative branch runs. Design `or-else`\nbranches to be independent or idempotent with respect to their writes.\n"}
@@ -349,7 +349,7 @@
 {"text": "<!-- FILE: docs/migration.md -->\n# Migrating from Gerbil to Jerboa\n\nThis guide covers porting Gerbil Scheme code to Jerboa (stock Chez Scheme).\n\n## What Works Unchanged\n\nMost Gerbil user-level code runs on Jerboa with only import changes:\n\n- `def`, `def*` with optional, keyword, and rest arguments\n- `defstruct`, `defclass`, `defmethod`\n- `match` (lists, cons, predicates, guards, `and`/`or`/`not`)\n- `try`/`catch`/`finally`\n- `while`, `until`, `when`, `unless`\n- `defrule`, `defrules`\n- Hash tables: `hash-ref`, `hash-put!`, `hash-get`, `hash-keys`, etc.\n- Method dispatch: `{method obj}` or `(~ obj 'method)`\n- `chain`, `chain-and`, `assert!`\n- `displayln`, `iota`, `1+`, `1-`\n- JSON: `read-json`, `write-json`\n- Path ops: `path-expand`, `path-join`, `path-directory`\n- String ops: `string-split`, `string-join`, `string-trim`\n- List ops: `take`, `drop`, `any`, `every`, `filter-map`\n\n## Import Translation\n\n### Module Paths\n\nGerbil's `:std/foo` becomes Jerboa's `(std foo)`:\n\n```scheme\n;; Gerbil\n(import :std/text/json :std/sort :std/misc/string)\n\n;; Jerboa\n(import (std text json) (std sort) (std misc string))\n```\n\nOr use the prelude for the most common modules:\n\n```scheme\n;; Gerbil\n(import :std/sugar :std/sort :std/format :std/text/json\n        :std/misc/string :std/misc/list :std/misc/alist :std/misc/ports)\n\n;; Jerboa \u2014 all of the above in one import\n(import (jerboa prelude))\n```\n\n### Gambit Compatibility\n\nFor code that uses Gambit primitives (`##` namespace, u8vector ops, etc.):\n\n```scheme\n;; Gerbil\n(import :gerbil/gambit)\n\n;; Jerboa\n(import (std gambit-compat))\n```\n\nThis provides 150+ compatibility symbols including `u8vector` ops, `void?`,\n`call-with-input-string`, `cpu-count`, and more.\n\n### Import Conflicts\n\nThe #1 friction point. Chez Scheme and Jerboa define some of the same names\nwith different semantics. See [import-conflicts.md](import-conflicts.md) for\nthe full matrix.\n\n**Quick solution \u2014 use `(jerboa prelude clean)`:**\n\n```scheme\n;; No conflicts \u2014 prelude/clean excludes names that collide with (chezscheme)\n(import (chezscheme) (jerboa prelude clean))\n```\n\n**Manual solution \u2014 use `except`:**\n\n```scheme\n(import (except (chezscheme)\n          make-hash-table hash-table?\n          sort sort! format printf fprintf\n          iota 1+ 1-\n          path-extension path-absolute?\n          with-input-from-string with-output-to-string)\n        (jerboa prelude))\n```\n\n## Mechanical Transformations\n\n### Keyword Arguments\n\nGerbil uses `#:keyword` syntax; Jerboa uses quoted symbols:\n\n```scheme\n;; Gerbil\n(http-get url headers: my-headers timeout: 30)\n\n;; Jerboa\n(http-get url headers: my-headers timeout: 30)\n;; Jerboa's `def` macro supports keyword: syntax natively\n```\n\n### `spawn` and Threading\n\n```scheme\n;; Gerbil\n(import :std/sugar)\n(spawn (lambda () (displayln \"hello\")))\n(spawn/name \"worker\" (lambda () (work)))\n\n;; Jerboa\n(import (std misc thread))\n(spawn (lambda () (displayln \"hello\")))\n(spawn/name \"worker\" (lambda () (work)))\n```\n\n### Channels\n\n```scheme\n;; Gerbil\n(import :std/misc/channel)\n(def ch (make-channel))\n(channel-put ch 42)\n(channel-get ch)\n\n;; Jerboa\n(import (std misc channel))\n(def ch (make-channel))\n(channel-put ch 42)\n(channel-get ch)\n```\n\n### Iterators\n\n```scheme\n;; Gerbil\n(import :std/iter)\n(for/collect ((x (in-range 10))) (* x x))\n\n;; Jerboa\n(import (std iter))\n(for/collect ((x (in-range 10))) (* x x))\n```\n\n### Actors\n\n```scheme\n;; Gerbil\n(import :std/actor)\n(defproto myproto (greeting name))\n\n;; Jerboa\n(import (std actor))\n(defprotocol myproto (greeting name))\n```\n\n## What Needs Redesign\n\n### Gerbil Expander API\n\nCode that uses `:gerbil/expander` (custom syntax transformers, module\nintrospection) has no Jerboa equivalent. This is internal Gerbil machinery,\nnot user-facing API. Alternatives:\n\n- Use Chez's `syntax-case` directly for macro work\n- Use `(std doc)` for module introspection\n\n### `##` Gambit Primitives\n\nMost `##` primitives are covered by `(std gambit-compat)`. For the rest,\ncheck `(chezscheme)` for native equivalents:\n\n| Gambit | Chez Scheme |\n|--------|-------------|\n| `##sys-clock` | `(cpu-time)` |\n| `##gc` | `(collect)` |\n| `##void` | `(void)` |\n| `##fixnum?` | `(fixnum? x)` |\n| `##car` | `(car x)` (Chez already inlines) |\n\n### `(export #t)` \u2014 Re-export All\n\nGerbil's `(export #t)` re-exports everything. Chez R6RS requires explicit\nexports. You must list all exported symbols in the library form.\n\n### `parameterize` Thread Locality\n\nGerbil's `parameterize` is not thread-local. Chez's `parameterize` IS\nthread-local (each thread gets its own parameter cell). This usually doesn't\nmatter, but if your code relies on one thread's `parameterize` being visible\nto another, you need explicit communication (channels, shared state).\n\n## The Source Translator\n\nFor bulk porting, Jerboa includes a source translator:\n\n```scheme\n(import (jerboa translator))\n\n;; Translate a Gerbil source file\n(translate-file \"input.ss\" \"output.ss\" (default-transforms))\n```\n\nThis handles:\n- `#:keyword` syntax normalization\n- `[x y]` bracket form translation\n- `##gambit-primitive` replacement\n- `defstruct` \u2192 R6RS record expansion\n- `let-hash` / `using` expansion\n- Import path translation\n- `try`/`catch` normalization\n\n## Common Pitfalls\n\n1. **Forgetting import exclusions** \u2014 If you get \"duplicate import\" errors,\n   check [import-conflicts.md](import-conflicts.md).\n\n2. **`match` on structs** \u2014 Core `match` doesn't destructure structs.\n   Import `(std match2)` for struct patterns with `define-match-type`.\n\n3. **`format` differences** \u2014 Jerboa's `format` returns a string (like\n   Gerbil). Chez's `format` writes to a port. If you import both, exclude\n   one.\n\n4. **`sort` stability** \u2014 Jerboa's `sort` is NOT guaranteed stable. Use\n   `stable-sort` when order of equal elements matters.\n\n5. **Thread primitives** \u2014 Chez's `fork-thread` works but Gerbil's `spawn`\n   API (with names, groups) is in `(std misc thread)`.\n"}
 {"text": "<!-- FILE: docs/TODO.md -->\n# Jerboa TODO\n\nUpdated 2026-03-21.\n\nTracks remaining work. Items marked DONE were either already implemented\nor completed in the March 2026 sprint. Remaining items are genuinely open.\n\n---\n\n## Completed\n\n### Developer Experience\n- [x] **CLI entry point** \u2014 `bin/jerboa` with run, test, eval, repl, build, version (`bin/jerboa`)\n- [x] **Error messages** \u2014 `(std errors)` with \"did you mean\" suggestions, Levenshtein distance, `install-error-handler!`\n- [x] **Import conflict ergonomics** \u2014 `(jerboa prelude clean)` conflict-free prelude + `docs/import-conflicts.md` reference\n\n### Language Features\n- [x] **String interpolation** \u2014 `(std interpolate)` macro: `(interpolate \"Hello ${name}\")`\n- [x] **Struct pattern matching** \u2014 `(std match2)` with `define-match-type`, sealed hierarchies, exhaustiveness\n- [x] **Iterator protocol** \u2014 `(std iter)` with `for/collect`, `for/fold`, `in-range`, `in-hash-*`, etc.\n- [x] **Interface/protocol system** \u2014 `(std interface)` with `definterface` + `(std generic)` with `defgeneric`\n\n### Standard Library\n- [x] **SRFI-1** (list library, 271 lines), **SRFI-13** (string library, 258 lines), **SRFI-14** (character sets), **SRFI-19** (date/time), **SRFI-43** (vectors), **SRFI-128** (comparators), **SRFI-141** (integer division)\n- [x] **Process management** \u2014 `(std misc process)` with `process-port-pid`, `process-kill`, `tty?`\n- [x] **Spawn/concurrency** \u2014 `(std misc thread)` with `spawn`, `spawn/name`, `spawn/group`, `thread-sleep!`\n- [x] **CLI framework** \u2014 `(std cli multicall)` subcommands, `(std cli style)` ANSI colors, `(std cli completion)` bash/zsh\n- [x] **Serialization** \u2014 `(std text toml)`, `(std text msgpack)`, `(std text cbor)`, `(std io)` structured I/O\n- [x] **Networking** \u2014 `(std net smtp)`, `(std net json-rpc)`, `(std net udp)`, `(std net address)`\n\n### Ecosystem & Tooling\n- [x] **Package manager** \u2014 `(jerboa pkg)` with semver + `(jerboa lock)` with lockfiles\n- [x] **Build system** \u2014 `(std build)` with module discovery, DAG ordering, content hashing\n- [x] **Source translator** \u2014 `(jerboa translator)` with 20+ transform functions\n\n### Validation & Trust\n- [x] **CI pipeline** \u2014 `.github/workflows/test.yml` (GitHub Actions)\n- [x] **Benchmark suite** \u2014 `benchmarks/bench-core.ss` (hash tables, match, sort, JSON, iterators, structs)\n- [x] **Example applications** \u2014 `examples/hello-api.ss`, `examples/cli-tool.ss`, `examples/data-pipeline.ss`, `examples/chat-server.ss`\n- [x] **Ports documentation** \u2014 `docs/ports.md`\n\n### Documentation\n- [x] **Quickstart guide** \u2014 `docs/quickstart.md`\n- [x] **Migration guide** \u2014 `docs/migration.md`\n- [x] **Import conflict reference** \u2014 `docs/import-conflicts.md`\n- [x] **API reference generator** \u2014 `tools/gen-api-docs.ss`\n\n- [x] **Struct patterns in prelude match** \u2014 `(jerboa prelude)` now re-exports `(std match2)`'s `match` with struct patterns, sealed hierarchies, active patterns, and `match/strict`\n- [x] **Package registry** \u2014 `(jerboa registry)` with GitHub-based install/uninstall/update; `bin/jerboa install/uninstall/update/list` CLI commands\n- [x] **LSP server** \u2014 `(std lsp server)` + `(std lsp symbols)` with JSON-RPC over stdio; `tools/jerboa-lsp.ss` entry point; completion, hover, go-to-definition, diagnostics\n- [x] **Protocol Buffers** \u2014 `(std protobuf)` wire format encoder/decoder (proto3); varint, fixed32/64, length-delimited, zigzag, embedded messages\n\n---\n\n## Remaining \u2014 Gerbil Parity Sprint (50 features)\n\nFeatures that Gerbil has but Jerboa does not. Organized by category.\n\n### Core Language (4)\n\n- [x] **1. `(std amb)`** \u2014 Nondeterministic computation with backtracking (amb, amb-find, amb-collect, amb-assert)\n- [x] **2. `(std lazy)`** \u2014 Lazy streams (lazy, delay, force, lcons, ltake, ldrop, lmap, lfilter, lfold, lappend)\n- [x] **3. `(std ref)`** \u2014 Generic polymorphic accessor (ref, ref-set! for lists, vectors, hash tables, strings)\n- [x] **4. `(std deprecation)`** \u2014 Deprecation warnings with source location and migration hints\n\n### Data Structures (7)\n\n- [x] **5. `(std misc plist)`** \u2014 Property lists (pget, pput, pdel, plist->alist, alist->plist, plist-keys)\n- [x] **6. `(std misc evector)`** \u2014 Expandable/growable vectors (make-evector, evector-push!, evector-pop!, evector-ref, evector->vector)\n- [x] **7. `(std misc rbtree)`** \u2014 Red-black balanced BST (rbtree-insert, rbtree-lookup, rbtree-delete, rbtree-fold, rbtree->list)\n- [x] **8. `(std misc decimal)`** \u2014 Exact decimal arithmetic (decimal+, decimal*, decimal/, decimal-round, string->decimal, decimal->string)\n- [x] **9. `(std misc prime)`** \u2014 Primality testing, prime generation, factorization (prime?, next-prime, primes-up-to, factorize)\n- [x] **10. `(std misc dag)`** \u2014 DAG operations (make-dag, dag-add-edge!, topological-sort, dag-reachable, dag-sources, dag-sinks)\n- [x] **11. `(std misc shared)`** \u2014 Thread-safe shared mutable state (make-shared, shared-ref, shared-set!, shared-update!, shared-cas!)\n\n### Text & Encoding (4)\n\n- [x] **12. `(std text utf16)`** \u2014 UTF-16 encoding/decoding (string->utf16, utf16->string, utf16-length, BOM handling)\n- [x] **13. `(std text utf32)`** \u2014 UTF-32 encoding/decoding (string->utf32, utf32->string)\n- [x] **14. `(std text base58)`** \u2014 Base58 encoding for Bitcoin/IPFS (base58-encode, base58-decode, base58check-encode)\n- [x] **15. `(std text html)`** \u2014 HTML parsing and generation (html-escape, html-unescape, parse-html, html->sxml)\n\n### Markup (3)\n\n- [x] **16. `(std markup sxml)`** \u2014 SXML representation, construction, and manipulation (sxml:element?, sxml:attributes, sxml:children)\n- [x] **17. `(std markup sxml-path)`** \u2014 XPath-like queries on SXML trees (sxpath, sxml:select, node-typeof?, node-join)\n- [x] **18. `(std markup sxml-print)`** \u2014 SXML to HTML/XML serialization (sxml->html, sxml->xml, sxml->string)\n\n### Networking (4)\n\n- [x] **19. `(std net socks)`** \u2014 SOCKS4/5 proxy client (socks-connect, socks4-connect, socks5-connect, with-socks-proxy)\n- [x] **20. `(std net s3)`** \u2014 AWS S3 client (s3-put-object, s3-get-object, s3-list-bucket, s3-delete-object, sigv4 signing)\n- [x] **21. `(std net sasl)`** \u2014 SASL authentication mechanisms (sasl-plain, sasl-scram-sha-256, sasl-step)\n- [x] **22. `(std net bio)`** \u2014 Buffered network I/O (make-bio-input, make-bio-output, bio-read-line, bio-read-bytes, bio-flush)\n\n### Web (2)\n\n- [x] **23. `(std web fastcgi)`** \u2014 FastCGI protocol (fastcgi-accept, fastcgi-respond, fastcgi-params)\n- [x] **24. `(std web rack)`** \u2014 Middleware/handler composable web interface (make-app, wrap-middleware, rack-run)\n\n### MIME (2)\n\n- [x] **25. `(std mime struct)`** \u2014 MIME message structure (make-mime-message, mime-headers, mime-body, multipart-encode, multipart-decode)\n- [x] **26. `(std mime types)`** \u2014 MIME type database (extension->mime-type, mime-type->extension, mime-type?)\n\n### Crypto (2)\n\n- [x] **27. `(std crypto bn)`** \u2014 Big number arithmetic (bn+, bn*, bn-mod, bn-expt-mod, bn->bytevector, bytevector->bn)\n- [x] **28. `(std crypto dh)`** \u2014 Diffie-Hellman key exchange (dh-generate-parameters, dh-generate-key, dh-compute-shared)\n\n### Database (2)\n\n- [x] **29. `(std db dbi)`** \u2014 Generic database interface (dbi-connect, dbi-query, dbi-exec, dbi-prepare, dbi-with-transaction)\n- [x] **30. `(std db conpool)`** \u2014 Database connection pooling (make-connection-pool, pool-acquire, pool-release, with-connection)\n\n### Parser Framework (2)\n\n- [x] **31. `(std parser deflexer)`** \u2014 Lexer definition macro (deflexer, define-token, lex-string, lex-port)\n- [x] **32. `(std parser defparser)`** \u2014 Parser definition macro with grammar rules (defparser, define-rule, parse)\n\n### Protobuf Enhanced (2)\n\n- [x] **33. `(std protobuf macros)`** \u2014 defmessage macro for defining proto3 message types as Scheme records\n- [x] **34. `(std protobuf grammar)`** \u2014 .proto file parser (read-proto-file, proto->scheme)\n\n### Debug (3)\n\n- [x] **35. `(std debug heap)`** \u2014 Heap introspection and GC stats (heap-size, gc-count, gc-time, object-counts)\n- [x] **36. `(std debug memleak)`** \u2014 Memory leak detection (track-allocation, report-leaks, with-leak-check)\n- [x] **37. `(std debug threads)`** \u2014 Thread inspection and debugging (thread-list, thread-backtrace, thread-state)\n\n### I/O (3)\n\n- [x] **38. `(std io bio)`** \u2014 Buffered I/O with lookahead (make-buffered-input, buffered-peek, buffered-read-line, buffered-unread)\n- [x] **39. `(std io strio)`** \u2014 String-based I/O with readers/writers (make-string-reader, reader-peek, reader-read-while)\n- [x] **40. `(std io delimited)`** \u2014 Delimited text I/O (read-delimited, read-line*, read-until, write-delimited)\n\n### CLI (1)\n\n- [x] **41. `(std cli print-exit)`** \u2014 Print formatted output and exit (print-exit, print-error-exit, exit/success, exit/failure)\n\n### SRFI (9)\n\n- [x] **42. `(std srfi srfi-41)`** \u2014 Streams: lazy sequences (stream-cons, stream-null, stream-car, stream-cdr, stream-map, stream-filter, stream-fold)\n- [x] **43. `(std srfi srfi-42)`** \u2014 Eager comprehensions (do-ec, list-ec, vector-ec, sum-ec, every?-ec, first-ec)\n- [x] **44. `(std srfi srfi-113)`** \u2014 Sets and bags (set, bag, set-adjoin, set-delete, set-union, set-intersection, set-difference)\n- [x] **45. `(std srfi srfi-121)`** \u2014 Generators (make-coroutine-generator, generator->list, gmap, gfilter, gfold)\n- [x] **46. `(std srfi srfi-132)`** \u2014 Sort libraries (list-sort, list-stable-sort, vector-sort, vector-stable-sort, list-merge, vector-merge)\n- [x] **47. `(std srfi srfi-133)`** \u2014 Extended vector library (vector-unfold, vector-map, vector-for-each, vector-fold, vector-count, vector-index)\n- [x] **48. `(std srfi srfi-134)`** \u2014 Immutable deques (ideque, ideque-front, ideque-back, ideque-add-front, ideque-add-back, ideque-remove-front)\n- [x] **49. `(std srfi srfi-144)`** \u2014 Flonum library (fl+, fl*, flsqrt, flexp, fllog, flsin, flcos, fl=, fl<, flonum constants)\n- [x] **50. `(std srfi srfi-158)`** \u2014 Generators and accumulator (generator, circular-generator, make-accumulator, generator-fold, generator-for-each)\n\n### Networking Polish (existing)\n\n- **HTTP client API compat** \u2014 header format differences vs Gerbil\n"}
 {"text": "<!-- FILE: docs/typing.md -->\n# Gradual Typing in Jerboa\n\n`(std typed)` and `(std typed advanced)` provide optional type annotations\nfor Chez Scheme code. Annotations compile to runtime assertions in `debug`\nmode and disappear entirely in `release` mode \u2014 zero overhead in production.\n\n---\n\n## Overview\n\nGradual typing lets you add type information incrementally. You can annotate\na single hot function, a whole module, or nothing at all. Untyped and typed\ncode interoperate freely: a typed function can call an untyped one and vice\nversa.\n\nThe philosophy is practical:\n\n- **Development**: catch type errors early with descriptive messages that\n  name the function, the argument, the expected type, and the actual value.\n- **Production**: flip `(*typed-mode* 'release)` and every annotation\n  vanishes from the compiled code. No allocation, no branches, no overhead.\n- **Incrementally adoptable**: annotate what matters, leave the rest alone.\n\n---\n\n## Importing the Library\n\n```scheme\n(import (std typed))\n```\n\nFor advanced features (occurrence typing, row polymorphism, refinement\ntypes, type-directed compilation):\n\n```scheme\n(import (std typed)\n        (std typed advanced))\n```\n\n---\n\n## Core API \u2014 `(std typed)`\n\n### `*typed-mode*`\n\nA Chez Scheme parameter that controls whether annotations are enforced.\n\n```scheme\n(*typed-mode*)           ; => 'debug   (default)\n(*typed-mode* 'debug)    ; enable runtime checks\n(*typed-mode* 'release)  ; strip all checks (zero overhead)\n(*typed-mode* 'none)     ; alias for 'release\n```\n\nAccepts exactly `debug`, `release`, or `none`; any other value raises an\nerror. Set this once at program startup, typically from a command-line flag\nor environment variable.\n\n---\n\n### `define/t` \u2014 typed define\n\n```\n(define/t (name [arg : type] ...) : ret-type body ...)\n(define/t (name [arg : type] ...) body ...)\n```\n\nDefines a function with annotated arguments and an optional return type.\nArguments without a `: type` annotation are implicitly typed `any` (no\ncheck). In `debug` mode the macro expands to argument checks before the body\nand a return-type check after. In `release` mode it expands to a plain\n`define`.\n\n```scheme\n(import (std typed))\n\n;; Both arguments and return type annotated\n(define/t (add-strings [a : string] [b : string]) : string\n  (string-append a b))\n\n;; Only arguments annotated (no return type check)\n(define/t (safe-divide [n : number] [d : number])\n  (if (zero? d)\n    (error 'safe-divide \"division by zero\")\n    (/ n d)))\n\n;; Mixed: one typed, one untyped\n(define/t (scale [xs : list] factor)\n  (map (lambda (x) (* x factor)) xs))\n```\n\nError message example (in `debug` mode):\n\n```\nException in add-strings: b: expected string, got 42\n```\n\n---\n\n### `lambda/t` \u2014 typed lambda\n\n```\n(lambda/t ([arg : type] ...) : ret-type body ...)\n(lambda/t ([arg : type] ...) body ...)\n```\n\nSame as `define/t` but produces an anonymous procedure.\n\n```scheme\n(define process\n  (lambda/t ([items : list] [transform : procedure]) : list\n    (map transform items)))\n\n;; Works naturally as a higher-order argument\n(sort items (lambda/t ([a : string] [b : string]) : boolean\n              (string<? a b)))\n```\n\n---\n\n### `assert-type` \u2014 inline type assertion\n\n```\n(assert-type expr type-name)\n```\n\nEvaluates `expr`, checks that its value satisfies the named type in `debug`\nmode, and returns the value. Useful for asserting types at arbitrary points\nin code without annotating a whole function.\n\n```scheme\n(define (read-config path)\n  (let ([raw (call-with-input-file path read)])\n    (assert-type raw list)     ; must be a list at the top level\n    (process-config raw)))\n```\n\n---\n\n### Built-in Type Names\n\nThe following type names are recognized out of the box:\n\n| Name          | Predicate         |\n|---------------|-------------------|\n| `fixnum`      | `fixnum?`         |\n| `flonum`      | `flonum?`         |\n| `string`      | `string?`         |\n| `pair`        | `pair?`           |\n| `vector`      | `vector?`         |\n| `bytevector`  | `bytevector?`     |\n| `boolean`     | `boolean?`        |\n| `char`        | `char?`           |\n| `symbol`      | `symbol?`         |\n| `list`        | `list?`           |\n| `number`      | `number?`         |\n| `integer`     | `integer?`        |\n| `real`        | `real?`           |\n| `any`         | _(no check)_      |\n\n#### Compound type specs\n\nIn addition to flat names, `type-predicate` understands these constructors:\n\n```scheme\n(listof string)          ; list whose every element is a string\n(vectorof fixnum)        ; vector whose every element is a fixnum\n(hashof symbol any)      ; hashtable (key/value types not checked per-entry)\n(-> string number)       ; any procedure (only arity/type not verified at runtime)\n```\n\n---\n\n### `register-type-predicate!` \u2014 extend the type registry\n\n```scheme\n(register-type-predicate! 'port port?)\n(register-type-predicate! 'positive-integer\n  (lambda (x) (and (integer? x) (positive? x))))\n```\n\nAfter registration the name works everywhere: `define/t`, `lambda/t`,\n`assert-type`, `check-type!`, etc.\n\n---\n\n### `type-predicate` \u2014 look up a predicate\n\n```scheme\n(type-predicate 'string)      ; => string?\n(type-predicate '(listof fixnum))  ; => compound predicate procedure\n(type-predicate 'unknown)     ; => #f\n```\n\nReturns `#f` for unrecognized type specs.\n\n---\n\n### `check-type!` and `check-return-type!` \u2014 procedural checks\n\n```scheme\n(check-type! who arg-name val type-name)\n(check-return-type! who val type-name)\n```\n\nThe primitive checkers used internally by `define/t` and `lambda/t`. You can\ncall them directly to build custom validation helpers. Both are no-ops when\n`(*typed-mode*)` is not `debug`.\n\n```scheme\n(define (validate-config! cfg)\n  (check-type! 'validate-config! 'cfg cfg 'list)\n  (for-each (lambda (entry)\n              (check-type! 'validate-config! 'entry entry 'pair))\n            cfg))\n```\n\n---\n\n### `with-fixnum-ops` \u2014 fixnum arithmetic specialization\n\n```\n(with-fixnum-ops body ...)\n```\n\nA syntax transformer that walks `body` at macro-expansion time and replaces\ngeneric arithmetic operators with their fixnum-specific variants:\n\n| Generic    | Fixnum    |\n|------------|-----------|\n| `+`        | `fx+`     |\n| `-`        | `fx-`     |\n| `*`        | `fx*`     |\n| `/`        | _(none)_  |\n| `<`        | `fx<`     |\n| `>`        | `fx>`     |\n| `<=`       | `fx<=`    |\n| `>=`       | `fx>=`    |\n| `=`        | `fx=`     |\n| `quotient` | `fxquotient` |\n| `remainder`| `fxremainder` |\n| `modulo`   | `fxmodulo` |\n| `abs`      | `fxabs`   |\n| `zero?`    | `fxzero?` |\n| `positive?`| `fxpositive?` |\n| `negative?`| `fxnegative?` |\n| `min`      | `fxmin`   |\n| `max`      | `fxmax`   |\n| `add1`     | `fx1+`    |\n| `sub1`     | `fx1-`    |\n\n```scheme\n(define (sum-fixnums xs)\n  (with-fixnum-ops\n    (let loop ([lst xs] [acc 0])\n      (if (null? lst)\n        acc\n        (loop (cdr lst) (+ acc (car lst)))))))\n```\n\nSpecial forms (`if`, `let`, `lambda`, `cond`, `when`, etc.) are traversed\nbut their heads are never rewritten. Only operator positions are affected.\n\n---\n\n### `with-flonum-ops` \u2014 flonum arithmetic specialization\n\n```\n(with-flonum-ops body ...)\n```\n\nSame idea as `with-fixnum-ops`, replacing generic operators with their\nflonum variants (`fl+`, `fl-`, `fl*`, `fl/`, `flsqrt`, `flsin`, `flcos`,\netc.). Includes transcendentals: `sqrt`, `floor`, `ceiling`, `round`,\n`truncate`, `sin`, `cos`, `tan`, `exp`, `log`.\n\n```scheme\n(define (euclidean-distance x1 y1 x2 y2)\n  (with-flonum-ops\n    (let ([dx (- x2 x1)]\n          [dy (- y2 y1)])\n      (sqrt (+ (* dx dx) (* dy dy))))))\n```\n\n---\n\n### Effect type annotations \u2014 `define/te` and `lambda/te`\n\n```\n(define/te (name [arg : type] ...) : (Effect EffectName ReturnType) body ...)\n(lambda/te ([arg : type] ...) : (Effect EffectName ReturnType) body ...)\n```\n\nEffect types document that a function performs a named side effect and\nreturns a value of `ReturnType`. At runtime only the argument types and\n`ReturnType` are checked; `EffectName` is informational for tooling and\ndocumentation.\n\n```scheme\n(define/te (write-record! [db : any] [rec : pair]) : (Effect IO boolean)\n  (db-insert! db rec))\n\n(define/te (fetch-user [id : fixnum]) : (Effect IO pair)\n  (db-query user-table id))\n```\n\nFallback: if no `(Effect ...)` wrapper is present, `define/te` behaves\nexactly like `define/t`.\n\n#### Effect type descriptors\n\n```scheme\n(make-effect-type effect-name result-type) ; => effect-type record\n(effect-type? x)                           ; => #t/#f\n(effect-type-effect et)                    ; => effect-name symbol\n(effect-type-result et)                    ; => result type symbol\n```\n\nThese records are available for tooling that wants to inspect annotations at\nruntime, but are not used internally by the assertion machinery.\n\n---\n\n## Advanced API \u2014 `(std typed advanced)`\n\n### Step 14: Occurrence Typing\n\nOccurrence typing narrows the type of a variable inside a conditional branch\nbased on the test predicate. The narrowing is inserted as an `assert-type`\ncall at the start of the branch body, making the type-checker (and the\nreader) aware that the variable is more specific within that branch.\n\nSupported narrowings:\n\n| Predicate      | Narrowed type  |\n|----------------|----------------|\n| `string?`      | `string`       |\n| `fixnum?`      | `fixnum`       |\n| `flonum?`      | `flonum`       |\n| `number?`      | `number`       |\n| `integer?`     | `integer`      |\n| `real?`        | `real`         |\n| `pair?`        | `pair`         |\n| `list?`        | `list`         |\n| `null?`        | `null`         |\n| `vector?`      | `vector`       |\n| `symbol?`      | `symbol`       |\n| `char?`        | `char`         |\n| `boolean?`     | `boolean`      |\n| `procedure?`   | `procedure`    |\n| `bytevector?`  | `bytevector`   |\n| `hashtable?`   | `hashtable`    |\n\n#### `if/t`\n\n```scheme\n(if/t test then else)\n```\n\nIn the `then` branch, if `test` is `(pred? var)` and `pred?` maps to a\nknown type, `var` is rebound with an `assert-type` annotation.\n\n```scheme\n(define (stringify x)\n  (if/t (string? x)\n    (string-append \"[\" x \"]\")   ; x narrowed to string here\n    (number->string x)))\n```\n\n#### `when/t`\n\n```scheme\n(when/t test body ...)\n```\n\nLike `if/t` but for one-armed conditionals.\n\n```scheme\n(define (maybe-upcase x)\n  (when/t (string? x)\n    (string-upcase x)))   ; x narrowed to string in body\n```\n\n#### `unless/t`\n\n```scheme\n(unless/t test body ...)\n```\n\nNegated form. The body runs when `test` is false. (Internally expands to\n`when/t (not test) ...`.)\n\n#### `cond/t`\n\n```scheme\n(cond/t ([test body ...] ...)\n        [else body ...])\n```\n\nMulti-branch version. Each clause whose test is a predicate application\nnarrows the tested variable in its body.\n\n```scheme\n(define (describe x)\n  (cond/t\n    [(string? x)  (string-append \"string: \" x)]\n    [(fixnum? x)  (string-append \"fixnum: \" (number->string x))]\n    [(pair? x)    (string-append \"pair of length \" (number->string (length x)))]\n    [else         \"unknown\"]))\n```\n\n---\n\n### Step 15: Row Polymorphism\n\nRow types express structural constraints: \"this object must have at least\nthese fields accessible by these accessors, with these types.\" Any record\ntype that satisfies the row shape passes the check, regardless of its\nnominal type.\n\n#### `defrow`\n\n```\n(defrow Name (field-name : Type) ...)\n```\n\nDefines a named row type and generates:\n\n- `Name?` \u2014 predicate that returns `#t` if the object satisfies the row\n- `check-Name!` \u2014 assertion that raises an error on failure\n\nField accessibility is checked at runtime by calling the accessor (resolved\nvia `eval` in the interaction environment) on the object and catching any\nexception.\n\n```scheme\n(defrow Printable\n  (to-string : procedure))\n\n(defrow Drawable\n  (draw!   : procedure)\n  (x       : number)\n  (y       : number))\n```\n\nAfter this:\n\n```scheme\n(Printable? my-obj)   ; => #t or #f\n(check-Printable! my-obj)  ; raises if not satisfied\n```\n\n#### `row-check`\n\n```\n(row-check obj-expr row-name)\n(row-check obj-expr (field-name : Type) ...)\n```\n\nInline row check without needing a named `defrow`. Returns `#t` or `#f`.\n\n```scheme\n;; Named row\n(row-check widget Drawable)\n\n;; Anonymous inline row\n(row-check config\n  (host : string)\n  (port : fixnum))\n```\n\n#### `row?`\n\n```scheme\n(row? 'Printable)   ; => row-type record or #f\n```\n\nLooks up a named row type from the registry.\n\n#### `row-type?`\n\nPredicate for the `row-type` record type itself (distinguishes row-type\ndescriptors from other values).\n\n---\n\n### Step 16: Refinement Types\n\nA refinement type combines a base type with an additional predicate. The\nvalue must satisfy both.\n\n#### `make-refinement-type`\n\n```scheme\n(make-refinement-type 'integer positive?)\n; => refinement-type record: base=integer, pred=positive?\n```\n\n#### Accessors\n\n```scheme\n(refinement-type? x)          ; predicate\n(refinement-type-base rt)     ; => base type symbol\n(refinement-type-pred rt)     ; => predicate procedure or symbol\n```\n\n#### `check-refinement!`\n\n```scheme\n(check-refinement! who name val base pred-spec)\n```\n\nIn `debug` mode: first checks that `val` satisfies `base`, then calls\n`pred-spec` on `val`. Either check failure raises an error. `pred-spec` may\nbe a procedure or a symbol (resolved via `eval`).\n\n#### `assert-refined`\n\n```\n(assert-refined expr base-type pred-expr)\n```\n\nInline macro version. Evaluates `expr`, runs both checks, and returns the\nvalue.\n\n```scheme\n(define (set-port! [p : fixnum])\n  (assert-refined p integer positive?)   ; must be a positive integer\n  ...)\n\n;; Or with a lambda predicate\n(define (bounded-index! [i : fixnum])\n  (assert-refined i fixnum (lambda (n) (and (>= n 0) (< n max-index))))\n  ...)\n```\n\n---\n\n### Step 17: Type-Directed Compilation\n\n`define/tc` and `lambda/tc` extend `define/t` and `lambda/t` with\nautomatic arithmetic specialization based on declared types.\n\n**Specialization rules:**\n\n- All arguments _and_ return type are `fixnum` \u2192 body wrapped in\n  `(with-fixnum-ops ...)` automatically\n- All arguments _and_ return type are `flonum` \u2192 body wrapped in\n  `(with-flonum-ops ...)`\n- Mixed or other types \u2192 no specialization (generic ops, return type\n  checked normally)\n\n```\n(define/tc (name [arg : type] ...) : ret-type body ...)\n(lambda/tc ([arg : type] ...) : ret-type body ...)\n```\n\n```scheme\n;; All fixnum \u2192 automatic fx+ fx- fx* rewriting, zero overhead in release\n(define/tc (dot-product-int [a : fixnum] [b : fixnum] [c : fixnum] [d : fixnum]) : fixnum\n  (+ (* a c) (* b d)))\n\n;; All flonum \u2192 automatic fl+ fl* etc.\n(define/tc (hypot [x : flonum] [y : flonum]) : flonum\n  (sqrt (+ (* x x) (* y y))))\n\n;; Mixed \u2192 no rewriting, just type checks\n(define/tc (format-value [n : number] [label : string]) : string\n  (string-append label \": \" (number->string n)))\n```\n\n`define/tc` also handles `(Refine base pred)` annotations in argument\nposition:\n\n```scheme\n(define/tc (sqrt-positive [x : (Refine flonum flpositive?)]) : flonum\n  (flsqrt x))\n```\n\nIn `debug` mode this checks both that `x` is a `flonum` and that\n`(flpositive? x)` holds before executing the body.\n\n---\n\n### Union and Intersection type specs\n\n```\n(Union T1 T2 ...)\n(Intersection T1 T2 ...)\n```\n\nThese produce predicate procedures at runtime. They can be passed to\n`register-type-predicate!` to give the combined type a name.\n\n```scheme\n(register-type-predicate! 'number-or-string\n  (Union number string))\n\n(register-type-predicate! 'positive-real\n  (Intersection real (lambda (x) (positive? x))))\n\n(define/t (display-value [x : number-or-string]) : string\n  (if (number? x) (number->string x) x))\n```\n\n---\n\n## Complete Example\n\n```scheme\n(import (chezscheme)\n        (std typed)\n        (std typed advanced))\n\n;; ---- Custom type ----\n(register-type-predicate! 'non-empty-string\n  (lambda (s) (and (string? s) (> (string-length s) 0))))\n\n;; ---- Basic typed define ----\n(define/t (greet [name : non-empty-string]) : string\n  (string-append \"Hello, \" name \"!\"))\n\n;; ---- Type-directed fixnum arithmetic ----\n(define/tc (factorial [n : fixnum]) : fixnum\n  (if (fxzero? n) 1 (fx* n (factorial (fx1- n)))))\n\n;; ---- Occurrence typing ----\n(define (safe-length x)\n  (cond/t\n    [(list?   x) (length x)]\n    [(string? x) (string-length x)]\n    [(vector? x) (vector-length x)]\n    [else (error 'safe-length \"unsupported type\" x)]))\n\n;; ---- Row polymorphism ----\n(defrow Named\n  (name : string))\n\n(define (print-name obj)\n  (check-Named! obj)\n  (display (name obj)))\n\n;; ---- Refinement type ----\n(define (safe-sqrt [x : (Refine flonum flpositive?)])\n  (define/tc ([x : (Refine flonum flpositive?)]) : flonum\n    (flsqrt x)))\n\n;; ---- Effect annotation ----\n(define/te (read-line! [port : any]) : (Effect IO string)\n  (get-line port))\n\n;; ---- Switch to release for production ----\n;; (*typed-mode* 'release)  ; all checks become no-ops\n```\n\n---\n\n## Performance Notes\n\n| Scenario | Recommendation |\n|---|---|\n| Development / CI | Keep `(*typed-mode* 'debug)` (default). Annotations catch bugs early with descriptive messages. |\n| Production binary | Set `(*typed-mode* 'release)` at startup or in your build entry point. |\n| Hot inner loops with fixnum math | Use `define/tc` with full fixnum annotations; get `fx*` etc. for free. |\n| Hot inner loops with flonum math | Use `define/tc` with full flonum annotations; get `fl*`, `flsqrt`, etc. |\n| Annotating library boundaries only | Annotate public-facing functions; leave internal helpers untyped or use `assert-type` at key points. |\n| `(listof T)` on large lists | The predicate walks every element. Prefer `list?` if the annotation is just documentation. |\n\n`with-fixnum-ops` and `with-flonum-ops` are pure compile-time rewrites \u2014\nthey have identical performance to writing `fx+` / `fl+` by hand.\n\nThe effect of `(*typed-mode* 'release)` is runtime, not compile-time. If\nyou need truly zero-overhead release builds, wrap `define/t` forms in a\n`when` on a compile-time constant, or use `define/tc` which only emits\nchecks inside `(when (eq? (*typed-mode*) 'debug) ...)` guards that Chez can\noptimize away.\n"}
-{"text": "<!-- FILE: docs/jerboa-edge.md -->\n# Jerboa Edge: The Killer App\n\n**Goal:** A single-file, production-grade webhook processing service that\ndemonstrates why Jerboa exists \u2014 Clojure's data philosophy, Erlang's fault\ntolerance, Go's deployment story, and built-in security, all in one 15MB binary.\n\n**Status:** 2026-04-12 \u2014 Phase 4 complete\n\n---\n\n## Why This, Why Now\n\nJerboa's stdlib is deep enough.  580 modules, 4 concurrency models, persistent\ndata structures, cryptography, a Rust native backend.  What's missing is a\n**single artifact that makes someone say \"I need this.\"**\n\nLanguages don't win on feature lists.  They win on one undeniable demo:\n\n- Ruby had Rails (2004) \u2014 \"blog in 15 minutes\" video got 1M views\n- Go had Docker (2013) \u2014 proved single-binary deployment scales\n- Erlang had ejabberd \u2014 proved \"never go down\" wasn't marketing\n- Elixir had Phoenix LiveView \u2014 proved you don't need a JS framework\n\nJerboa needs its equivalent.  Not a framework \u2014 a **runnable, self-contained\nservice** that a Clojure developer can clone, read in 20 minutes, deploy in 60\nseconds, and immediately understand what they'd gain by switching.\n\n### Why a Webhook Service\n\nWebhook processing is the ideal demo because:\n\n1. **Everyone needs one.** Stripe, GitHub, Slack, Shopify, Twilio \u2014 every modern\n   SaaS pushes webhooks.  Every engineering team has a janky webhook handler.\n\n2. **It naturally requires everything Jerboa is good at:**\n   - High-concurrency HTTP ingestion (fiber-httpd)\n   - Fault-tolerant async processing (actors with supervision)\n   - Ordered message pipelines (CSP channels + transducers)\n   - Shared mutable state without locks (STM)\n   - Real-time monitoring (WebSocket dashboard)\n   - Sandboxed user-defined filters (Landlock)\n   - Zero-dependency deployment (static binary)\n\n3. **The Clojure comparison is devastating.**  In Clojure, you need: JVM + Redis\n   (for queuing) + Sidekiq/Celery (for processing) + nginx (for TLS/routing) +\n   Docker Compose to wire it all together.  4 processes, 2GB RAM, 30-second cold\n   starts.  In Jerboa, it's one file, one binary, 20MB of RAM, instant startup.\n\n4. **It's small enough to be readable** but complex enough to be credible.  Target:\n   ~380 lines of Scheme, not 3000.\n\n---\n\n## What It Demonstrates\n\n| Feature shown | Jerboa module | Why it matters |\n|---|---|---|\n| Accept 100K concurrent webhooks | `(std net fiber-httpd)` | Fibers, not threads \u2014 4KB each |\n| Route by path + method | `(std net router)` | Clean URL dispatch with params |\n| Parse/validate JSON payloads | `(std text json)` | Zero-dependency JSON |\n| Queue work without Redis | `(std csp clj)` | CSP channels are built in |\n| Transform payloads in flight | `(std transducer)` | Composable, no intermediate allocs |\n| Process with fault tolerance | `(std actor)` | Supervision trees restart crashes |\n| Coordinate shared state | `(std stm)` | STM refs, not mutexes |\n| Store in persistent collections | `(std pmap)`, `(std pvec)` | Immutable audit trail |\n| Stream live updates | `(std net fiber-ws)` | WebSocket on same server |\n| Sandbox user filters | `(std security sandbox)` | Landlock/seccomp per eval |\n| Manage lifecycle | `(std component)` | Clean start/stop ordering |\n| Deploy anywhere | `make static` | One musl binary, scp it |\n\n---\n\n## Architecture\n\n```\n                    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                    \u2502      fiber-httpd           \u2502\n                    \u2502   (one fiber per conn)     \u2502\n                    \u2502   epoll-backed accept      \u2502\n                    \u2514\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                          \u2502            \u2502\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u25bc                                    \u25bc\n     POST /hooks/:type                    GET /dashboard\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n     \u2502 1. Parse JSON    \u2502                 \u2502 WebSocket       \u2502\n     \u2502 2. Validate sig  \u2502                 \u2502 upgrade         \u2502\n     \u2502 3. Respond 202   \u2502                 \u2502 (fiber-ws)      \u2502\n     \u2502 4. Enqueue       \u2502                 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                         \u2502\n              \u2502                                   \u2502\n              \u25bc                                   \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u2502\n     \u2502 Ingest Channel           \u2502                 \u2502\n     \u2502 (std csp clj)            \u2502                 \u2502\n     \u2502                          \u2502                 \u2502\n     \u2502 Transducer pipeline:     \u2502                 \u2502\n     \u2502  validate \u2192 normalize    \u2502                 \u2502\n     \u2502  \u2192 deduplicate \u2192 route   \u2502                 \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                 \u2502\n              \u2502                                   \u2502\n              \u25bc                                   \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u2502\n     \u2502 Worker Pool              \u2502                 \u2502\n     \u2502 (actor supervisor)       \u2502                 \u2502\n     \u2502                          \u2502                 \u2502\n     \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510   \u2502                 \u2502\n     \u2502 \u2502worker-1\u2502 \u2502worker-2\u2502..N\u2502                 \u2502\n     \u2502 \u2514\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518   \u2502                 \u2502\n     \u2502     \u2502          \u2502        \u2502                 \u2502\n     \u2502  process    process     \u2502                 \u2502\n     \u2502  webhook    webhook     \u2502                 \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                 \u2502\n              \u2502                                  \u2502\n              \u25bc                                  \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                \u2502\n     \u2502 State Store              \u2502                \u2502\n     \u2502 (STM refs + pmap)        \u2502     updates    \u2502\n     \u2502                          \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n     \u2502 Immutable snapshots      \u2502\n     \u2502 of all webhook events    \u2502\n     \u2502 + processing results     \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n### Data Flow\n\n1. **Ingest:** fiber-httpd receives POST, one fiber handles the connection.\n   Handler parses JSON, validates HMAC signature, writes a `202 Accepted`\n   response immediately, then enqueues the event onto the ingest channel.  The\n   fiber is freed in microseconds \u2014 no blocking on processing.\n\n2. **Pipeline:** The ingest channel has a transducer stack attached:\n   - `(filtering valid-signature?)` \u2014 drop replayed/forged events\n   - `(mapping normalize-event)` \u2014 canonicalize field names, timestamps\n   - `(deduplicate)` \u2014 idempotency via event ID\n   - `(mapping route-event)` \u2014 tag with destination handler\n\n3. **Processing:** A supervisor manages N worker actors.  Each worker pulls from\n   the channel, executes the handler, writes results to the state store.  If a\n   worker crashes (bad payload, handler bug), the supervisor restarts it.  The\n   other workers continue unaffected.\n\n4. **State:** All webhook events and results live in STM-managed persistent maps.\n   `(dosync (alter events conj new-event))`.  No locks, no race conditions.\n   Every state transition is an immutable snapshot \u2014 you get a full audit trail\n   for free.\n\n5. **Dashboard:** A WebSocket endpoint streams state changes to connected\n   browsers.  Each WS connection is a fiber.  When the STM refs change, watchers\n   push updates to all connected dashboard fibers.\n\n6. **Sandboxing (optional):** Users can define filter expressions that run in a\n   Landlock sandbox.  `(run-safe-eval user-filter-code config)` executes in a\n   forked child with syscall restrictions.  The parent process is never at risk.\n\n---\n\n## API Design\n\n### Starting the Service\n\n```scheme\n#!/usr/bin/env scheme --libdirs lib --script\n(import (jerboa prelude)\n        (std clojure)\n        (std component)\n        (std net fiber-httpd)\n        (std net fiber-ws)\n        (std net router)\n        (std csp clj)\n        (std actor)\n        (std stm)\n        (std transducer)\n        (std text json)\n        (std crypto hmac))\n\n(def (main)\n  (let ([sys (-> (system-map\n                   'config    (component 'config)\n                   'state     (component 'state)\n                   'ingest    (component 'ingest)\n                   'workers   (component 'workers)\n                   'http      (component 'http))\n                 (system-using\n                   '((state   . (config))\n                     (ingest  . (config state))\n                     (workers . (config state ingest))\n                     (http    . (config state ingest)))))])\n    (displayln \"[edge] starting on port 8080...\")\n    (start sys)))\n\n(main)\n```\n\n### Webhook Ingestion Endpoint\n\n```scheme\n(def (handle-webhook req)\n  ;; 1. Parse the JSON body\n  (let* ([body     (request-body req)]\n         [payload  (string->json-object body)]\n         [type     (route-param req \"type\")]\n         [sig      (request-header req \"x-signature\")])\n\n    ;; 2. Verify HMAC signature\n    (unless (verify-hmac-sha256 secret sig body)\n      (respond-json 401 \"{\\\"error\\\":\\\"invalid signature\\\"}\"))\n\n    ;; 3. Build internal event record\n    (let ([event (hash-map\n                   \"id\"         (hash-ref payload \"id\" (uuid))\n                   \"type\"       type\n                   \"payload\"    payload\n                   \"received\"   (datetime->iso8601 (datetime-now))\n                   \"status\"     \"queued\")])\n\n      ;; 4. Enqueue for async processing \u2014 this returns immediately\n      (>!! ingest-ch event)\n\n      ;; 5. Respond 202 before processing starts\n      (respond-json 202\n        (json-object->string\n          (hash-map \"status\" \"queued\"\n                    \"id\"     (hash-ref event \"id\")))))))\n```\n\n### Transducer Pipeline\n\n```scheme\n(def ingest-xf\n  (compose-transducers\n    ;; Drop events with duplicate IDs (idempotency)\n    (deduplicate)\n\n    ;; Normalize: ensure all events have a timestamp\n    (mapping\n      (lambda (evt)\n        (if (hash-key? evt \"timestamp\")\n          evt\n          (let ([ht (make-hash-table)])\n            (hash-for-each (lambda (k v) (hash-put! ht k v)) evt)\n            (hash-put! ht \"timestamp\" (hash-ref evt \"received\"))\n            ht))))\n\n    ;; Drop obviously malformed events\n    (filtering\n      (lambda (evt)\n        (and (hash-key? evt \"type\")\n             (hash-key? evt \"payload\"))))))\n\n;; Create the ingest channel with the transducer attached\n(def ingest-ch (chan 4096 ingest-xf))\n```\n\n### Supervised Worker Pool\n\n```scheme\n(def (make-worker-behavior state-ref ingest-ch handlers)\n  ;; Each worker is an actor that loops reading from the channel\n  (lambda (msg)\n    (match msg\n      ['start\n       ;; Enter processing loop\n       (let loop ()\n         (let ([event (<!! ingest-ch)])\n           (when event\n             ;; Look up the handler for this event type\n             (let* ([type    (hash-ref event \"type\" \"unknown\")]\n                    [handler (hash-ref handlers type default-handler)]\n                    [result  (try\n                               (ok (handler event))\n                               (catch (e)\n                                 (err (error-message e))))])\n\n               ;; Record result in STM state\n               (dosync\n                 (alter state-ref\n                   (lambda (st)\n                     (let ([id (hash-ref event \"id\")])\n                       (hash-put! st id\n                         (hash-map\n                           \"event\"     event\n                           \"result\"    result\n                           \"processed\" (datetime->iso8601\n                                         (datetime-now))))\n                       st))))\n\n               ;; Notify dashboard watchers\n               (notify-watchers! event result))\n\n             (loop))))]\n\n      ;; Graceful shutdown\n      ['stop (void)])))\n\n(def (start-worker-pool n state-ref ingest-ch handlers)\n  (let ([children\n         (map (lambda (i)\n                (make-child-spec\n                  (string->symbol (format \"worker-~a\" i))\n                  (lambda ()\n                    (let ([ref (spawn-actor\n                                 (make-worker-behavior\n                                   state-ref ingest-ch handlers)\n                                 (format \"worker-~a\" i))])\n                      (send ref 'start)\n                      ref))\n                  'permanent     ;; always restart on crash\n                  5              ;; 5s shutdown grace\n                  'worker))\n              (iota n))])\n    (start-supervisor 'one-for-one children 10 60)))\n```\n\n### STM State Store\n\n```scheme\n;; The entire application state is one STM ref holding a persistent map.\n;; Every mutation is atomic, consistent, and produces an immutable snapshot.\n\n(def state-ref (make-ref (make-hash-table)))\n\n;; Read current state (outside transaction \u2014 snapshot)\n(def (get-event id)\n  (hash-get (ref-deref state-ref) id))\n\n;; Read all events for a type\n(def (get-events-by-type type)\n  (let ([st (ref-deref state-ref)])\n    (filter\n      (lambda (entry)\n        (let ([evt (hash-ref (cdr entry) \"event\")])\n          (equal? (hash-ref evt \"type\") type)))\n      (hash->list st))))\n\n;; Stats: count by status\n(def (event-stats)\n  (let ([st (ref-deref state-ref)])\n    (let ([total   0]\n          [ok-n    0]\n          [err-n   0])\n      (hash-for-each\n        (lambda (id record)\n          (set! total (+ total 1))\n          (let ([r (hash-ref record \"result\")])\n            (if (ok? r)\n              (set! ok-n (+ ok-n 1))\n              (set! err-n (+ err-n 1)))))\n        st)\n      (hash-map \"total\" total\n                \"ok\"    ok-n\n                \"error\" err-n))))\n```\n\n### WebSocket Dashboard\n\n```scheme\n;; Connected dashboard clients \u2014 a simple list protected by STM\n(def watchers-ref (make-ref '()))\n\n(def (handle-dashboard req fd poller)\n  ;; Upgrade HTTP \u2192 WebSocket\n  (let ([ws (fiber-ws-upgrade (request-headers req) fd poller)])\n    (when ws\n      ;; Register this connection\n      (dosync (alter watchers-ref (lambda (ws-list) (cons ws ws-list))))\n\n      ;; Send current state snapshot\n      (fiber-ws-send ws\n        (json-object->string (event-stats)))\n\n      ;; Keep alive \u2014 recv loop handles pings and detects disconnect\n      (let loop ()\n        (let ([msg (fiber-ws-recv ws)])\n          (cond\n            [(not msg)\n             ;; Client disconnected \u2014 unregister\n             (dosync\n               (alter watchers-ref\n                 (lambda (ws-list)\n                   (filter (lambda (w) (not (eq? w ws))) ws-list))))]\n            [else (loop)]))))))\n\n;; Called by workers after processing an event\n(def (notify-watchers! event result)\n  (let ([update (json-object->string\n                  (hash-map\n                    \"type\"   \"event-processed\"\n                    \"id\"     (hash-ref event \"id\")\n                    \"status\" (if (ok? result) \"ok\" \"error\")\n                    \"time\"   (datetime->iso8601 (datetime-now))))])\n    (for-each\n      (lambda (ws)\n        (guard (_ [#t (void)])  ;; ignore send errors on dead connections\n          (fiber-ws-send ws update)))\n      (ref-deref watchers-ref))))\n```\n\n### Sandboxed User Filters\n\n```scheme\n;; Users can submit filter expressions that run in a Landlock sandbox.\n;; The filter receives the event as a hash table binding `evt` and must\n;; return #t (accept) or #f (drop).\n\n(def (eval-user-filter filter-code event)\n  (let ([code (format\n                \"(let ([evt '~s]) ~a)\"\n                (hash->list event)\n                filter-code)]\n        [config (make-sandbox-config\n                  'timeout 2           ;; 2 second hard limit\n                  'seccomp 'pure-computation  ;; no I/O, no network\n                  'max-output-size 1024)])\n    (run-safe-eval code config)))\n\n;; Example usage:\n;; User submits: (equal? (cdr (assoc \"type\" evt)) \"payment.completed\")\n;; This runs in a forked child with:\n;;   - No filesystem access (Landlock)\n;;   - No network syscalls (seccomp)\n;;   - 2 second timeout\n;;   - Parent process is never at risk\n```\n\n### HTTP Router Assembly\n\n```scheme\n(def (make-edge-router state-ref ingest-ch)\n  (let ([r (make-router)])\n\n    ;; Webhook ingestion\n    (router-post! r \"/hooks/:type\"\n      (lambda (req) (handle-webhook req)))\n\n    ;; Query API\n    (router-get! r \"/api/events/:id\"\n      (lambda (req)\n        (let ([id (route-param req \"id\")])\n          (aif (get-event id)\n            (respond-json 200 (json-object->string it))\n            (respond-json 404 \"{\\\"error\\\":\\\"not found\\\"}\")))))\n\n    (router-get! r \"/api/stats\"\n      (lambda (req)\n        (respond-json 200\n          (json-object->string (event-stats)))))\n\n    ;; Dashboard WebSocket\n    (router-get! r \"/dashboard\"\n      (lambda (req)\n        (make-websocket-response\n          (lambda (fd poller req)\n            (handle-dashboard req fd poller)))))\n\n    ;; Health check\n    (router-get! r \"/health\"\n      (lambda (req)\n        (respond-json 200 \"{\\\"status\\\":\\\"ok\\\"}\")))\n\n    r))\n```\n\n### Full Startup\n\n```scheme\n(def (run-edge port num-workers)\n  (let* ([state-ref   (make-ref (make-hash-table))]\n         [ingest-ch   (chan 4096 ingest-xf)]\n         [handlers    (make-hash-table)]\n         [router      (make-edge-router state-ref ingest-ch)])\n\n    ;; Register webhook handlers by type\n    (hash-put! handlers \"payment.completed\"\n      (lambda (evt)\n        (displayln \"[payment] processing \" (hash-ref evt \"id\"))))\n\n    (hash-put! handlers \"user.created\"\n      (lambda (evt)\n        (displayln \"[user] processing \" (hash-ref evt \"id\"))))\n\n    ;; Start supervised worker pool\n    (let ([supervisor\n           (start-worker-pool num-workers state-ref ingest-ch handlers)])\n\n      ;; Start HTTP server on fibers\n      (displayln \"[edge] listening on :\" port)\n      (displayln \"[edge] \" num-workers \" workers supervised\")\n      (displayln \"[edge] dashboard at ws://localhost:\" port \"/dashboard\")\n\n      (fiber-httpd-start port\n        (lambda (req)\n          (let ([match (router-match router\n                         (request-method req)\n                         (request-path-only req))])\n            (if match\n              ((route-match-handler match) req)\n              (respond-json 404 \"{\\\"error\\\":\\\"not found\\\"}\"))))))))\n\n;; Entry point\n(run-edge 8080 4)\n```\n\n---\n\n## The Comparison That Sells It\n\n### Clojure Equivalent\n\nTo build the same webhook processor in Clojure, you need:\n\n```\nproject.clj (or deps.edn)          \u2014 dependency management\n\u251c\u2500\u2500 JVM 17+                        \u2014 200MB runtime\n\u251c\u2500\u2500 ring/ring-core                 \u2014 HTTP abstraction\n\u251c\u2500\u2500 http-kit or aleph              \u2014 async HTTP server\n\u251c\u2500\u2500 compojure or reitit            \u2014 routing\n\u251c\u2500\u2500 cheshire                       \u2014 JSON parsing\n\u251c\u2500\u2500 core.async                     \u2014 channels\n\u251c\u2500\u2500 mount or component             \u2014 lifecycle\n\u251c\u2500\u2500 redis (Carmine)                \u2014 job queue\n\u251c\u2500\u2500 sidekiq or machinery           \u2014 background workers\n\u251c\u2500\u2500 timbre                         \u2014 logging\n\u2514\u2500\u2500 Docker + docker-compose.yml    \u2014 deployment\n```\n\n**Result:**\n- 12+ dependencies (each with transitive deps \u2192 100+ JARs)\n- 3 processes minimum (JVM app + Redis + worker process)\n- 30-second cold start (JVM + dependency loading)\n- 512MB-2GB RAM (JVM heap + Redis)\n- Docker image: 400MB-1GB\n- `docker-compose up` to run locally\n- 5-10 files of boilerplate before writing business logic\n\n### Jerboa Equivalent\n\n```\nwebhook-service.ss                  \u2014 the entire service\n```\n\n**Result:**\n- 0 external dependencies (everything is stdlib)\n- 1 process (one binary does HTTP + workers + state + WebSocket)\n- Instant startup (Chez native code, no interpreter warmup)\n- 20-40MB RAM (no GC pressure from boxed JVM objects)\n- Static binary: 15MB\n- `scp edge user@server: && ssh server ./edge` to deploy\n- ~380 lines, one file, zero config files\n\n### Side-by-Side: Processing a Webhook\n\n**Clojure:**\n```clojure\n;; deps: ring, compojure, cheshire, core.async, carmine (redis)\n\n(ns webhook.handler\n  (:require [compojure.core :refer [defroutes POST]]\n            [ring.middleware.json :refer [wrap-json-body]]\n            [cheshire.core :as json]\n            [clojure.core.async :as async]\n            [taoensso.carmine :as redis]))\n\n(defroutes app-routes\n  (POST \"/hooks/:type\" [type :as req]\n    (let [payload (:body req)]\n      ;; Push to Redis queue (external process!)\n      (redis/wcar {} (redis/lpush \"webhooks\" (json/encode payload)))\n      {:status 202\n       :body (json/encode {:status \"queued\"})})))\n\n;; Separate worker process reads from Redis:\n(defn worker []\n  (loop []\n    (let [raw (redis/wcar {} (redis/brpop \"webhooks\" 0))\n          event (json/decode (second raw) true)]\n      (process-event! event)\n      (recur))))\n```\n\n**Jerboa:**\n```scheme\n;; deps: none (everything is stdlib)\n\n(import (jerboa prelude)\n        (std clojure)\n        (std net fiber-httpd)\n        (std csp clj)\n        (std text json))\n\n(def work-ch (chan 4096))\n\n(def (handle-webhook req)\n  (let ([payload (string->json-object (request-body req))])\n    (>!! work-ch payload)\n    (respond-json 202 \"{\\\"status\\\":\\\"queued\\\"}\")))\n\n;; Workers are fibers on the same process \u2014 no Redis, no IPC\n(dotimes (i 4)\n  (go (let loop ()\n        (let ([event (<!! work-ch)])\n          (when event\n            (process-event! event)\n            (loop))))))\n\n(fiber-httpd-start 8080 handle-webhook)\n```\n\nThe Jerboa version is **12 lines** vs Clojure's **20+ lines + Redis + a separate\nworker process**.  And the Jerboa version handles more concurrent connections\n(fibers vs threads), has built-in fault tolerance (supervisor), and deploys as\none file.\n\n---\n\n## Implementation Plan\n\n### Phase 1: Core Demo (the ~380-line file) \u2014 COMPLETE\n\n**Deliverable:** [`jerboa-edge/edge.ss`](https://github.com/ober/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://github.com/ober/jerboa-edge\ncd jerboa-edge\nmake run\n\n# In another terminal:\ncurl -X POST localhost:8080/hooks/payment.completed \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"evt_123\",\"amount\":4999}'\n\ncurl localhost:8080/api/stats\n# => {\"total\":1,\"ok\":1,\"error\":0}\n```\n\n**Scope:**\n\n- [x] 1.1 **HTTP server with routing** \u2014 fiber-httpd with `:param` pattern routing.\n    Five endpoints: `POST /hooks/:type`, `GET /api/events/:id`, `GET /api/stats`,\n    `GET /health`, `GET /dashboard`.\n\n- [x] 1.2 **Channel-based ingestion** \u2014 CSP channel with transducer pipeline\n    (validate + dedup-by-ID + normalize).  Decouples accept from process.\n\n- [x] 1.3 **Actor-supervised workers** \u2014 N worker actors under a one-for-one\n    supervisor.  Each pulls from the channel, processes, writes to state.\n    `POST /hooks/test.crash` deliberately crashes a worker to show supervisor restart.\n\n- [x] 1.4 **STM state store** \u2014 All state in `(make-ref (make-persistent-map))`.\n    Atomic updates via `dosync`/`alter` with `(std pmap)` persistent maps.\n    Immutable snapshots \u2014 safe for concurrent reads during writes.\n\n- [x] 1.5 **WebSocket dashboard** \u2014 Single WS endpoint that streams\n    event-processed notifications to connected clients.  Sends initial stats\n    on connect.\n\n- [x] 1.6 **HMAC signature verification** \u2014 Validate `X-Signature` header using\n    `(std crypto native)` HMAC-SHA256.  Constant-time comparison.\n    Configurable via `EDGE_SECRET` env var.\n\n**Non-goals for Phase 1:**\n- Persistence across restarts (in-memory only)\n- TLS termination (use a reverse proxy, or wait for Phase 3)\n- Static binary packaging (Phase 3)\n\n**Success criteria:**\n- [x] Runs with `make run` (382 lines of Scheme)\n- [x] Handles 10K concurrent webhook POSTs without dropped events (verified: 9,993/10,000 accepted at 200 concurrent connections, ~454 req/sec)\n- [x] Worker crash + supervisor restart visible in logs\n- [x] WebSocket dashboard receives live event stream (verified with websocket-client)\n- [x] `curl` examples work out of the box (17/17 smoke tests pass)\n- [x] HMAC: no-sig and wrong-sig return 401, valid-sig returns 202\n\n### Phase 2: Programmability\n\n**Deliverable:** Sandboxed user filter support + handler registry\n\n- [x] 2.1 **Sandboxed eval** \u2014 Users submit filter expressions via\n    `POST /api/filters`.  Filters evaluated per-event via\n    `restricted-eval-string` (allowlist-only: no I/O, no FFI) with a\n    2-second engine-based timeout (no fork \u2014 fiber-safe).  Events that\n    fail any filter are dropped silently before processing.\n\n    ```\n    POST /api/filters  {\"name\":\"payments-only\",\"code\":\"(equal? (cdr (assoc \\\"type\\\" evt)) \\\"payment.completed\\\")\"}\n    GET  /api/filters\n    DELETE /api/filters/:name\n    ```\n\n    Key implementation detail: event data is bound as `evt` (a deep\n    alist), so filter code uses `assoc` with string keys (not `assq`).\n\n- [x] 2.2 **Hot handler registration** \u2014 New webhook types registered at\n    runtime via `POST /api/handlers`.  No restart required.  Handler\n    code runs in a custom restricted environment (`safe-bindings` +\n    `displayln`) with a 5-second engine timeout.  Exceptions propagate\n    to the worker's retry machinery.\n\n    ```\n    POST /api/handlers  {\"type\":\"order.shipped\",\"code\":\"(displayln \\\"shipped: \\\" (cdr (assoc \\\"id\\\" evt)))\"}\n    GET  /api/handlers\n    ```\n\n- [x] 2.3 **Retry with exponential backoff** \u2014 Failed events re-enter the\n    channel with an incremented retry count.  Workers delay via\n    `fork-thread` + `sleep` (non-blocking, OS-thread safe).  Dedup\n    transducer passes retried events (`retry-count > 0`).  After 3\n    retries, events go to a dead-letter STM store.\n\n    ```\n    GET  /api/dead-letter\n    POST /api/dead-letter/:id/replay\n    ```\n\n    Replay sets `retry-count: 1` to bypass the dedup filter.\n\n**Success criteria:**\n- [x] Filter code cannot call `open-file`, `socket`, or any I/O (allowlist env)\n- [x] Filter with 2s timeout verified: `with-timeout` uses Chez engines, no fork\n- [x] Hot-registered handlers process events without service restart (verified: `order.shipped`, `inventory.updated`)\n- [x] Failed events retry 3\u00d7 with 1s/2s/4s backoff then dead-letter (verified in log)\n- [x] Dead-letter replay re-processes event and clears queue (verified: status \u2192 \"ok\")\n- [x] Runs with `make run` (690 lines of Scheme)\n\n### Phase 3: Production Hardening \u2713\n\n**Deliverable:** Static binary, TLS, persistence, metrics (~1048 lines)\n\n- [x] 3.1 **Static musl binary** \u2014 `make edge-static` produces a fully static\n    ELF via `docker run jerboa21/jerboa` (musl Chez + Rust toolchain + WPO).\n    **Verified: 12MB, zero runtime dependencies.**  All native FFI symbols\n    registered via `Sforeign_symbol`; `JERBOA_STATIC=1` auto-set at startup\n    to bypass `load-shared-object` calls.  Deploy with `scp edge host:/usr/local/bin/`.\n\n- [x] 3.2 **TLS termination** \u2014 Optional `EDGE_TLS_CERT` / `EDGE_TLS_KEY`\n    env vars for direct HTTPS.  Uses `(std net tls-rustls)` \u2014 the Rust\n    rustls backend.  `make run-tls` target.  One OS thread per TLS connection;\n    same router handler for both plain and TLS paths.\n\n- [x] 3.3 **SQLite persistence** \u2014 `EDGE_DB_PATH` env var enables persistence\n    via `(std db sqlite-native)` (bundled rusqlite, no external libsqlite3).\n    WAL mode for concurrent reads.  Events and dead-letters loaded into STM\n    on startup.  All writes are fire-and-forget (guarded; SQLite error never\n    crashes a worker).  `make run-db` target.\n\n- [x] 3.4 **Prometheus metrics** \u2014 `/metrics` endpoint in Prometheus text\n    format via `(std metrics)`.  Exposed counters and histogram:\n    - `edge_events_received_total`\n    - `edge_events_ok_total`\n    - `edge_events_error_total`\n    - `edge_events_dead_letter_total`\n    - `edge_worker_restarts_total`\n    - `edge_processing_duration_seconds` (histogram, default buckets)\n    - `edge_active_connections` (gauge)\n\n- [x] 3.5 **Structured JSON logging** \u2014 All event-path log lines are JSON\n    (`jlog` helper, `json-object->string`).  Fields: `ts`, `msg`, plus\n    context fields (`worker`, `id`, `type`, `status`, `ms`, `err`).\n    Startup banner stays human-readable.\n\n- [x] 3.6 **Graceful shutdown** \u2014 `SIGTERM` triggers via `add-signal-handler!`\n    from `(std os signal)`.  Sequence: `fiber-httpd-stop!` \u2192 `chan-close!`\n    (workers see EOF) \u2192 3s drain window \u2192 `PRAGMA wal_checkpoint(FULL)` \u2192\n    `(exit 0)`.  Verified: completes in ~3s.\n\n**Success criteria:**\n- [x] `make edge-static` builds and runs \u2014 12MB, fully static, verified functional\n- [x] Graceful shutdown completes in ~3 seconds (verified)\n- [x] Prometheus scrape at `/metrics` returns well-formed text format (verified)\n- [x] SQLite events survive restart: `total:2 ok:2` after reload (verified)\n- [x] JSON log lines include worker, event id, type, status, duration (verified)\n- [x] Runs with `make run` (~1048 lines of Scheme)\n\n### Phase 4: Distribution \u2713\n\n**Deliverable:** Multi-node webhook processing via authenticated TCP transport and in-process Raft (~1244 lines)\n\n- [x] 4.1 **Clustered actors via `(std actor transport)`** \u2014 When\n    `EDGE_CLUSTER_COOKIE` is set, each node registers itself with\n    `start-node!` and starts a TCP server with `start-node-server!`.\n    Transport uses HMAC-SHA256 4-step challenge-response authentication\n    with per-message integrity (derived session key).  Each node spawns\n    a named `'edge-remote-ingest` actor that receives `(work event)`\n    messages from peers and enqueues them into the local ingest channel.\n    After HTTP starts, `connect-to-peers!` calls `GET /api/cluster/info`\n    on each peer to discover actor IDs, then builds remote refs via\n    `make-remote-actor-ref`.  Incoming webhooks are round-robined across\n    self + connected peers; failed peer sends fall back to local.\n    Wire format: `[4-byte BE length][fasl body][32-byte HMAC-SHA256]`.\n\n    ```bash\n    # Terminal 1 \u2014 node A (HTTP :8080, transport :9000)\n    make run-cluster-a\n\n    # Terminal 2 \u2014 node B (HTTP :8081, transport :9001)\n    make run-cluster-b\n\n    # POST to either node; events processed on either worker pool\n    curl -X POST localhost:8080/hooks/payment.completed -d '{\"id\":\"e1\",...}'\n    curl localhost:8080/api/cluster/info   # shows node_id, actor_id, peers\n    ```\n\n- [x] 4.2 **Replicated event ordering via `(std raft)`** \u2014 When\n    `EDGE_RAFT_SIZE > 0`, an N-node in-process Raft cluster runs alongside\n    the service.  Every accepted webhook is proposed to the Raft commit\n    log by the current leader (`raft-propose!`), providing a consensus-\n    ordered event sequence.  The commit log accumulates in-memory and is\n    available via `raft-log`/`raft-commit-index` on the leader node.\n\n    Note: `(std raft)` uses in-memory channels (no TCP) \u2014 it is an\n    in-process simulation designed to be bridged over a transport layer\n    for true cross-process Raft.  `(std crdt)` does not exist in the\n    stdlib; per-node STM with coordinator routing is used instead.\n\n    Note: `(std raft)` uses in-memory channels \u2014 cross-process Raft\n    would require bridging node inboxes over `(std actor transport)`.\n    `(std crdt)` does not exist in the stdlib.\n\n    ```bash\n    make run-raft RAFT_SIZE=3\n    curl localhost:8080/api/cluster/info\n    # => {\"raft_nodes\":3,\"raft_leader\":\"yes\",\"raft_term\":3,\"raft_commit\":5,...}\n    ```\n\n- [x] 4.3 **`GET /api/cluster/info`** \u2014 New endpoint returns full cluster\n    state: `node_id`, `ingest_actor_id`, `cluster_enabled`, `peers_connected`,\n    `raft_nodes`, `raft_leader`, `raft_term`, `raft_commit`.  Used by\n    peers at startup to discover actor IDs for remote refs.\n\n- [x] 4.4 **Transport shutdown** \u2014 `graceful-shutdown!` calls\n    `transport-shutdown!` (closes all authenticated TCP connections) and\n    `raft-stop!` on each Raft node before exit.\n\n**Success criteria:**\n- [x] `EDGE_CLUSTER_COOKIE=x make run` starts transport server on port 9000\n- [x] `make run-raft RAFT_SIZE=3` shows \"leader: elected, term: 3\" in logs\n- [x] `/api/cluster/info` returns raft_leader, raft_term, raft_commit (verified)\n- [x] Webhook processed correctly with Raft enabled (verified: status \"ok\")\n- [x] Graceful shutdown closes transport + stops Raft nodes\n- [x] Runs with `make run` (~1244 lines of Scheme)\n\n---\n\n## Module Dependency Map\n\nOnly stdlib modules \u2014 zero external dependencies:\n\n```\nexamples/webhook-service.ss\n\u251c\u2500\u2500 (jerboa prelude)              ;; base language\n\u251c\u2500\u2500 (std clojure)                 ;; polymorphic ops, inc/dec, etc.\n\u251c\u2500\u2500 (std component)               ;; lifecycle management\n\u251c\u2500\u2500 (std net fiber-httpd)         ;; HTTP server (fiber-native)\n\u2502   \u2514\u2500\u2500 (std net io)              ;; epoll-backed fiber I/O\n\u251c\u2500\u2500 (std net fiber-ws)            ;; WebSocket (fiber-native)\n\u251c\u2500\u2500 (std net router)              ;; URL routing with params\n\u251c\u2500\u2500 (std csp clj)                 ;; Clojure-style channels\n\u2502   \u251c\u2500\u2500 (std csp)                 ;; base CSP\n\u2502   \u251c\u2500\u2500 (std csp select)          ;; alts!/timeout\n\u2502   \u251c\u2500\u2500 (std csp fiber-chan)      ;; fiber-aware channels\n\u2502   \u2514\u2500\u2500 (std fiber)               ;; M:N fiber runtime\n\u251c\u2500\u2500 (std actor)                   ;; actors + supervision\n\u251c\u2500\u2500 (std stm)                     ;; transactional memory\n\u251c\u2500\u2500 (std transducer)              ;; composable transforms\n\u251c\u2500\u2500 (std text json)               ;; JSON parser/writer\n\u251c\u2500\u2500 (std crypto hmac)             ;; HMAC-SHA256 verification\n\u251c\u2500\u2500 (std security sandbox)        ;; Landlock/seccomp eval\n\u2514\u2500\u2500 (std pmap)                    ;; persistent hash maps\n```\n\nAll of these ship with Jerboa.  `(std crypto hmac)` and\n`(std security sandbox)` use the Rust native backend; everything else is\npure Scheme.\n\n---\n\n## Benchmarking Targets\n\nThese numbers establish what \"production-grade\" means for the demo:\n\n| Metric | Target | How to measure |\n|---|---|---|\n| Webhook throughput | 50K events/sec sustained | `wrk -c 1000 -t 8 -d 30s` |\n| p99 accept latency | < 5ms | Measure time from connect to 202 |\n| Memory (100K queued) | < 100MB | `/proc/self/status` VmRSS |\n| Worker restart time | < 50ms | Log timestamp delta after kill |\n| WebSocket fan-out | 1000 clients, < 10ms broadcast | Measure from STM commit to WS send |\n| Cold start | < 100ms | Time from exec to first request served |\n| Binary size | < 25MB | `ls -la edge` |\n| Concurrent connections | 100K+ | `ulimit -n` + `wrk` |\n\n### Benchmark Script\n\n```bash\n#!/bin/bash\n# bench-edge.sh \u2014 run against a live edge instance\n\necho \"=== Throughput ===\"\nwrk -c 500 -t 4 -d 10s -s post.lua http://localhost:8080/hooks/test\n\necho \"=== Latency ===\"\nwrk -c 1 -t 1 -d 5s -s post.lua --latency http://localhost:8080/hooks/test\n\necho \"=== Memory ===\"\nps -o rss= -p $(pgrep -f webhook-service) | awk '{print $1/1024 \"MB\"}'\n\necho \"=== Stats ===\"\ncurl -s localhost:8080/api/stats | python3 -m json.tool\n```\n\n---\n\n## What This Proves\n\nWhen someone sees this demo and asks \"why not just use Clojure?\", the answer\nis concrete:\n\n1. **Deploy in 5 seconds, not 5 minutes.** One binary, scp, run.  No JVM, no\n   Docker, no Redis, no config files.\n\n2. **20MB of RAM, not 2GB.**  Fibers are 4KB each.  Persistent maps share\n   structure.  No JVM heap overhead, no Redis process.\n\n3. **100ms cold start, not 30 seconds.**  Chez compiles to native code.  No\n   classloading, no dependency injection framework initialization.\n\n4. **Fault tolerance is built in, not bolted on.**  Actor supervision trees\n   restart crashed workers.  STM prevents data races.  You don't need to\n   add \"resilience4j\" as a dependency.\n\n5. **Security is built in, not bolted on.**  Sandboxed eval uses kernel-level\n   isolation (Landlock/seccomp).  No \"eval is dangerous\" disclaimer \u2014 eval\n   is sandboxed by default.\n\n6. **One file, ~1244 lines.**  Phase 1 was ~380 lines.  Phase 4 adds TLS,\n   Prometheus metrics, SQLite persistence, graceful shutdown, distributed\n   actor transport with HMAC-SHA256 auth, and in-process Raft consensus \u2014\n   and the result is still one file a senior engineer can read in an\n   afternoon.  Try that with a Spring Boot webhook processor.\n\nThis isn't a toy.  It handles 50K events/sec, restarts crashed workers in\n50ms, streams live updates over WebSocket, scrapes cleanly into Prometheus,\nand the entire thing fits in a single file with zero external dependencies.\n\n---\n\n## Related Work\n\n- `docs/green-wins.md` \u2014 fiber-httpd production hardening roadmap\n- `docs/clojure-left.md` \u2014 Clojure gap analysis and priority roadmap\n- `docs/clojure-vs-jerboa.md` \u2014 feature parity comparison\n- `examples/hello-api.ss` \u2014 simple HTTP API example (starting point)\n- `examples/chat-server.ss` \u2014 WebSocket + concurrency example\n- `lib/std/component.sls` \u2014 Stuart Sierra lifecycle (used in Phase 1)\n"}
+{"text": "<!-- FILE: docs/jerboa-edge.md -->\n# Jerboa Edge: The Killer App\n\n**Goal:** A single-file, production-grade webhook processing service that\ndemonstrates why Jerboa exists \u2014 Clojure's data philosophy, Erlang's fault\ntolerance, Go's deployment story, and built-in security, all in one 15MB binary.\n\n**Status:** 2026-04-12 \u2014 Phase 4 complete\n\n---\n\n## Why This, Why Now\n\nJerboa's stdlib is deep enough.  580 modules, 4 concurrency models, persistent\ndata structures, cryptography, a Rust native backend.  What's missing is a\n**single artifact that makes someone say \"I need this.\"**\n\nLanguages don't win on feature lists.  They win on one undeniable demo:\n\n- Ruby had Rails (2004) \u2014 \"blog in 15 minutes\" video got 1M views\n- Go had Docker (2013) \u2014 proved single-binary deployment scales\n- Erlang had ejabberd \u2014 proved \"never go down\" wasn't marketing\n- Elixir had Phoenix LiveView \u2014 proved you don't need a JS framework\n\nJerboa needs its equivalent.  Not a framework \u2014 a **runnable, self-contained\nservice** that a Clojure developer can clone, read in 20 minutes, deploy in 60\nseconds, and immediately understand what they'd gain by switching.\n\n### Why a Webhook Service\n\nWebhook processing is the ideal demo because:\n\n1. **Everyone needs one.** Stripe, GitHub, Slack, Shopify, Twilio \u2014 every modern\n   SaaS pushes webhooks.  Every engineering team has a janky webhook handler.\n\n2. **It naturally requires everything Jerboa is good at:**\n   - High-concurrency HTTP ingestion (fiber-httpd)\n   - Fault-tolerant async processing (actors with supervision)\n   - Ordered message pipelines (CSP channels + transducers)\n   - Shared mutable state without locks (STM)\n   - Real-time monitoring (WebSocket dashboard)\n   - Sandboxed user-defined filters (Landlock)\n   - Zero-dependency deployment (static binary)\n\n3. **The Clojure comparison is devastating.**  In Clojure, you need: JVM + Redis\n   (for queuing) + Sidekiq/Celery (for processing) + nginx (for TLS/routing) +\n   Docker Compose to wire it all together.  4 processes, 2GB RAM, 30-second cold\n   starts.  In Jerboa, it's one file, one binary, 20MB of RAM, instant startup.\n\n4. **It's small enough to be readable** but complex enough to be credible.  Target:\n   ~380 lines of Scheme, not 3000.\n\n---\n\n## What It Demonstrates\n\n| Feature shown | Jerboa module | Why it matters |\n|---|---|---|\n| Accept 100K concurrent webhooks | `(std net fiber-httpd)` | Fibers, not threads \u2014 4KB each |\n| Route by path + method | `(std net router)` | Clean URL dispatch with params |\n| Parse/validate JSON payloads | `(std text json)` | Zero-dependency JSON |\n| Queue work without Redis | `(std csp clj)` | CSP channels are built in |\n| Transform payloads in flight | `(std transducer)` | Composable, no intermediate allocs |\n| Process with fault tolerance | `(std actor)` | Supervision trees restart crashes |\n| Coordinate shared state | `(std stm)` | STM refs, not mutexes |\n| Store in persistent collections | `(std pmap)`, `(std pvec)` | Immutable audit trail |\n| Stream live updates | `(std net fiber-ws)` | WebSocket on same server |\n| Sandbox user filters | `(std security sandbox)` | Landlock/seccomp per eval |\n| Manage lifecycle | `(std component)` | Clean start/stop ordering |\n| Deploy anywhere | `make static` | One musl binary, scp it |\n\n---\n\n## Architecture\n\n```\n                    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                    \u2502      fiber-httpd           \u2502\n                    \u2502   (one fiber per conn)     \u2502\n                    \u2502   epoll-backed accept      \u2502\n                    \u2514\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                          \u2502            \u2502\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u25bc                                    \u25bc\n     POST /hooks/:type                    GET /dashboard\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n     \u2502 1. Parse JSON    \u2502                 \u2502 WebSocket       \u2502\n     \u2502 2. Validate sig  \u2502                 \u2502 upgrade         \u2502\n     \u2502 3. Respond 202   \u2502                 \u2502 (fiber-ws)      \u2502\n     \u2502 4. Enqueue       \u2502                 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                         \u2502\n              \u2502                                   \u2502\n              \u25bc                                   \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u2502\n     \u2502 Ingest Channel           \u2502                 \u2502\n     \u2502 (std csp clj)            \u2502                 \u2502\n     \u2502                          \u2502                 \u2502\n     \u2502 Transducer pipeline:     \u2502                 \u2502\n     \u2502  validate \u2192 normalize    \u2502                 \u2502\n     \u2502  \u2192 deduplicate \u2192 route   \u2502                 \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                 \u2502\n              \u2502                                   \u2502\n              \u25bc                                   \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                 \u2502\n     \u2502 Worker Pool              \u2502                 \u2502\n     \u2502 (actor supervisor)       \u2502                 \u2502\n     \u2502                          \u2502                 \u2502\n     \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510   \u2502                 \u2502\n     \u2502 \u2502worker-1\u2502 \u2502worker-2\u2502..N\u2502                 \u2502\n     \u2502 \u2514\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518   \u2502                 \u2502\n     \u2502     \u2502          \u2502        \u2502                 \u2502\n     \u2502  process    process     \u2502                 \u2502\n     \u2502  webhook    webhook     \u2502                 \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                 \u2502\n              \u2502                                  \u2502\n              \u25bc                                  \u2502\n     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                \u2502\n     \u2502 State Store              \u2502                \u2502\n     \u2502 (STM refs + pmap)        \u2502     updates    \u2502\n     \u2502                          \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n     \u2502 Immutable snapshots      \u2502\n     \u2502 of all webhook events    \u2502\n     \u2502 + processing results     \u2502\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n### Data Flow\n\n1. **Ingest:** fiber-httpd receives POST, one fiber handles the connection.\n   Handler parses JSON, validates HMAC signature, writes a `202 Accepted`\n   response immediately, then enqueues the event onto the ingest channel.  The\n   fiber is freed in microseconds \u2014 no blocking on processing.\n\n2. **Pipeline:** The ingest channel has a transducer stack attached:\n   - `(filtering valid-signature?)` \u2014 drop replayed/forged events\n   - `(mapping normalize-event)` \u2014 canonicalize field names, timestamps\n   - `(deduplicate)` \u2014 idempotency via event ID\n   - `(mapping route-event)` \u2014 tag with destination handler\n\n3. **Processing:** A supervisor manages N worker actors.  Each worker pulls from\n   the channel, executes the handler, writes results to the state store.  If a\n   worker crashes (bad payload, handler bug), the supervisor restarts it.  The\n   other workers continue unaffected.\n\n4. **State:** All webhook events and results live in STM-managed persistent maps.\n   `(dosync (alter events conj new-event))`.  No locks, no race conditions.\n   Every state transition is an immutable snapshot \u2014 you get a full audit trail\n   for free.\n\n5. **Dashboard:** A WebSocket endpoint streams state changes to connected\n   browsers.  Each WS connection is a fiber.  When the STM refs change, watchers\n   push updates to all connected dashboard fibers.\n\n6. **Sandboxing (optional):** Users can define filter expressions that run in a\n   Landlock sandbox.  `(run-safe-eval user-filter-code config)` executes in a\n   forked child with syscall restrictions.  The parent process is never at risk.\n\n---\n\n## API Design\n\n### Starting the Service\n\n```scheme\n#!/usr/bin/env scheme --libdirs lib --script\n(import (jerboa prelude)\n        (std clojure)\n        (std component)\n        (std net fiber-httpd)\n        (std net fiber-ws)\n        (std net router)\n        (std csp clj)\n        (std actor)\n        (std stm)\n        (std transducer)\n        (std text json)\n        (std crypto hmac))\n\n(def (main)\n  (let ([sys (-> (system-map\n                   'config    (component 'config)\n                   'state     (component 'state)\n                   'ingest    (component 'ingest)\n                   'workers   (component 'workers)\n                   'http      (component 'http))\n                 (system-using\n                   '((state   . (config))\n                     (ingest  . (config state))\n                     (workers . (config state ingest))\n                     (http    . (config state ingest)))))])\n    (displayln \"[edge] starting on port 8080...\")\n    (start sys)))\n\n(main)\n```\n\n### Webhook Ingestion Endpoint\n\n```scheme\n(def (handle-webhook req)\n  ;; 1. Parse the JSON body\n  (let* ([body     (request-body req)]\n         [payload  (string->json-object body)]\n         [type     (route-param req \"type\")]\n         [sig      (request-header req \"x-signature\")])\n\n    ;; 2. Verify HMAC signature\n    (unless (verify-hmac-sha256 secret sig body)\n      (respond-json 401 \"{\\\"error\\\":\\\"invalid signature\\\"}\"))\n\n    ;; 3. Build internal event record\n    (let ([event (hash-map\n                   \"id\"         (hash-ref payload \"id\" (uuid))\n                   \"type\"       type\n                   \"payload\"    payload\n                   \"received\"   (datetime->iso8601 (datetime-now))\n                   \"status\"     \"queued\")])\n\n      ;; 4. Enqueue for async processing \u2014 this returns immediately\n      (>!! ingest-ch event)\n\n      ;; 5. Respond 202 before processing starts\n      (respond-json 202\n        (json-object->string\n          (hash-map \"status\" \"queued\"\n                    \"id\"     (hash-ref event \"id\")))))))\n```\n\n### Transducer Pipeline\n\n```scheme\n(def ingest-xf\n  (compose-transducers\n    ;; Drop events with duplicate IDs (idempotency)\n    (deduplicate)\n\n    ;; Normalize: ensure all events have a timestamp\n    (mapping\n      (lambda (evt)\n        (if (hash-key? evt \"timestamp\")\n          evt\n          (let ([ht (make-hash-table)])\n            (hash-for-each (lambda (k v) (hash-put! ht k v)) evt)\n            (hash-put! ht \"timestamp\" (hash-ref evt \"received\"))\n            ht))))\n\n    ;; Drop obviously malformed events\n    (filtering\n      (lambda (evt)\n        (and (hash-key? evt \"type\")\n             (hash-key? evt \"payload\"))))))\n\n;; Create the ingest channel with the transducer attached\n(def ingest-ch (chan 4096 ingest-xf))\n```\n\n### Supervised Worker Pool\n\n```scheme\n(def (make-worker-behavior state-ref ingest-ch handlers)\n  ;; Each worker is an actor that loops reading from the channel\n  (lambda (msg)\n    (match msg\n      ['start\n       ;; Enter processing loop\n       (let loop ()\n         (let ([event (<!! ingest-ch)])\n           (when event\n             ;; Look up the handler for this event type\n             (let* ([type    (hash-ref event \"type\" \"unknown\")]\n                    [handler (hash-ref handlers type default-handler)]\n                    [result  (try\n                               (ok (handler event))\n                               (catch (e)\n                                 (err (error-message e))))])\n\n               ;; Record result in STM state\n               (dosync\n                 (alter state-ref\n                   (lambda (st)\n                     (let ([id (hash-ref event \"id\")])\n                       (hash-put! st id\n                         (hash-map\n                           \"event\"     event\n                           \"result\"    result\n                           \"processed\" (datetime->iso8601\n                                         (datetime-now))))\n                       st))))\n\n               ;; Notify dashboard watchers\n               (notify-watchers! event result))\n\n             (loop))))]\n\n      ;; Graceful shutdown\n      ['stop (void)])))\n\n(def (start-worker-pool n state-ref ingest-ch handlers)\n  (let ([children\n         (map (lambda (i)\n                (make-child-spec\n                  (string->symbol (format \"worker-~a\" i))\n                  (lambda ()\n                    (let ([ref (spawn-actor\n                                 (make-worker-behavior\n                                   state-ref ingest-ch handlers)\n                                 (format \"worker-~a\" i))])\n                      (send ref 'start)\n                      ref))\n                  'permanent     ;; always restart on crash\n                  5              ;; 5s shutdown grace\n                  'worker))\n              (iota n))])\n    (start-supervisor 'one-for-one children 10 60)))\n```\n\n### STM State Store\n\n```scheme\n;; The entire application state is one STM ref holding a persistent map.\n;; Every mutation is atomic, consistent, and produces an immutable snapshot.\n\n(def state-ref (make-ref (make-hash-table)))\n\n;; Read current state (outside transaction \u2014 snapshot)\n(def (get-event id)\n  (hash-get (ref-deref state-ref) id))\n\n;; Read all events for a type\n(def (get-events-by-type type)\n  (let ([st (ref-deref state-ref)])\n    (filter\n      (lambda (entry)\n        (let ([evt (hash-ref (cdr entry) \"event\")])\n          (equal? (hash-ref evt \"type\") type)))\n      (hash->list st))))\n\n;; Stats: count by status\n(def (event-stats)\n  (let ([st (ref-deref state-ref)])\n    (let ([total   0]\n          [ok-n    0]\n          [err-n   0])\n      (hash-for-each\n        (lambda (id record)\n          (set! total (+ total 1))\n          (let ([r (hash-ref record \"result\")])\n            (if (ok? r)\n              (set! ok-n (+ ok-n 1))\n              (set! err-n (+ err-n 1)))))\n        st)\n      (hash-map \"total\" total\n                \"ok\"    ok-n\n                \"error\" err-n))))\n```\n\n### WebSocket Dashboard\n\n```scheme\n;; Connected dashboard clients \u2014 a simple list protected by STM\n(def watchers-ref (make-ref '()))\n\n(def (handle-dashboard req fd poller)\n  ;; Upgrade HTTP \u2192 WebSocket\n  (let ([ws (fiber-ws-upgrade (request-headers req) fd poller)])\n    (when ws\n      ;; Register this connection\n      (dosync (alter watchers-ref (lambda (ws-list) (cons ws ws-list))))\n\n      ;; Send current state snapshot\n      (fiber-ws-send ws\n        (json-object->string (event-stats)))\n\n      ;; Keep alive \u2014 recv loop handles pings and detects disconnect\n      (let loop ()\n        (let ([msg (fiber-ws-recv ws)])\n          (cond\n            [(not msg)\n             ;; Client disconnected \u2014 unregister\n             (dosync\n               (alter watchers-ref\n                 (lambda (ws-list)\n                   (filter (lambda (w) (not (eq? w ws))) ws-list))))]\n            [else (loop)]))))))\n\n;; Called by workers after processing an event\n(def (notify-watchers! event result)\n  (let ([update (json-object->string\n                  (hash-map\n                    \"type\"   \"event-processed\"\n                    \"id\"     (hash-ref event \"id\")\n                    \"status\" (if (ok? result) \"ok\" \"error\")\n                    \"time\"   (datetime->iso8601 (datetime-now))))])\n    (for-each\n      (lambda (ws)\n        (guard (_ [#t (void)])  ;; ignore send errors on dead connections\n          (fiber-ws-send ws update)))\n      (ref-deref watchers-ref))))\n```\n\n### Sandboxed User Filters\n\n```scheme\n;; Users can submit filter expressions that run in a Landlock sandbox.\n;; The filter receives the event as a hash table binding `evt` and must\n;; return #t (accept) or #f (drop).\n\n(def (eval-user-filter filter-code event)\n  (let ([code (format\n                \"(let ([evt '~s]) ~a)\"\n                (hash->list event)\n                filter-code)]\n        [config (make-sandbox-config\n                  'timeout 2           ;; 2 second hard limit\n                  'seccomp 'pure-computation  ;; no I/O, no network\n                  'max-output-size 1024)])\n    (run-safe-eval code config)))\n\n;; Example usage:\n;; User submits: (equal? (cdr (assoc \"type\" evt)) \"payment.completed\")\n;; This runs in a forked child with:\n;;   - No filesystem access (Landlock)\n;;   - No network syscalls (seccomp)\n;;   - 2 second timeout\n;;   - Parent process is never at risk\n```\n\n### HTTP Router Assembly\n\n```scheme\n(def (make-edge-router state-ref ingest-ch)\n  (let ([r (make-router)])\n\n    ;; Webhook ingestion\n    (router-post! r \"/hooks/:type\"\n      (lambda (req) (handle-webhook req)))\n\n    ;; Query API\n    (router-get! r \"/api/events/:id\"\n      (lambda (req)\n        (let ([id (route-param req \"id\")])\n          (aif (get-event id)\n            (respond-json 200 (json-object->string it))\n            (respond-json 404 \"{\\\"error\\\":\\\"not found\\\"}\")))))\n\n    (router-get! r \"/api/stats\"\n      (lambda (req)\n        (respond-json 200\n          (json-object->string (event-stats)))))\n\n    ;; Dashboard WebSocket\n    (router-get! r \"/dashboard\"\n      (lambda (req)\n        (make-websocket-response\n          (lambda (fd poller req)\n            (handle-dashboard req fd poller)))))\n\n    ;; Health check\n    (router-get! r \"/health\"\n      (lambda (req)\n        (respond-json 200 \"{\\\"status\\\":\\\"ok\\\"}\")))\n\n    r))\n```\n\n### Full Startup\n\n```scheme\n(def (run-edge port num-workers)\n  (let* ([state-ref   (make-ref (make-hash-table))]\n         [ingest-ch   (chan 4096 ingest-xf)]\n         [handlers    (make-hash-table)]\n         [router      (make-edge-router state-ref ingest-ch)])\n\n    ;; Register webhook handlers by type\n    (hash-put! handlers \"payment.completed\"\n      (lambda (evt)\n        (displayln \"[payment] processing \" (hash-ref evt \"id\"))))\n\n    (hash-put! handlers \"user.created\"\n      (lambda (evt)\n        (displayln \"[user] processing \" (hash-ref evt \"id\"))))\n\n    ;; Start supervised worker pool\n    (let ([supervisor\n           (start-worker-pool num-workers state-ref ingest-ch handlers)])\n\n      ;; Start HTTP server on fibers\n      (displayln \"[edge] listening on :\" port)\n      (displayln \"[edge] \" num-workers \" workers supervised\")\n      (displayln \"[edge] dashboard at ws://localhost:\" port \"/dashboard\")\n\n      (fiber-httpd-start port\n        (lambda (req)\n          (let ([match (router-match router\n                         (request-method req)\n                         (request-path-only req))])\n            (if match\n              ((route-match-handler match) req)\n              (respond-json 404 \"{\\\"error\\\":\\\"not found\\\"}\"))))))))\n\n;; Entry point\n(run-edge 8080 4)\n```\n\n---\n\n## The Comparison That Sells It\n\n### Clojure Equivalent\n\nTo build the same webhook processor in Clojure, you need:\n\n```\nproject.clj (or deps.edn)          \u2014 dependency management\n\u251c\u2500\u2500 JVM 17+                        \u2014 200MB runtime\n\u251c\u2500\u2500 ring/ring-core                 \u2014 HTTP abstraction\n\u251c\u2500\u2500 http-kit or aleph              \u2014 async HTTP server\n\u251c\u2500\u2500 compojure or reitit            \u2014 routing\n\u251c\u2500\u2500 cheshire                       \u2014 JSON parsing\n\u251c\u2500\u2500 core.async                     \u2014 channels\n\u251c\u2500\u2500 mount or component             \u2014 lifecycle\n\u251c\u2500\u2500 redis (Carmine)                \u2014 job queue\n\u251c\u2500\u2500 sidekiq or machinery           \u2014 background workers\n\u251c\u2500\u2500 timbre                         \u2014 logging\n\u2514\u2500\u2500 Docker + docker-compose.yml    \u2014 deployment\n```\n\n**Result:**\n- 12+ dependencies (each with transitive deps \u2192 100+ JARs)\n- 3 processes minimum (JVM app + Redis + worker process)\n- 30-second cold start (JVM + dependency loading)\n- 512MB-2GB RAM (JVM heap + Redis)\n- Docker image: 400MB-1GB\n- `docker-compose up` to run locally\n- 5-10 files of boilerplate before writing business logic\n\n### Jerboa Equivalent\n\n```\nwebhook-service.ss                  \u2014 the entire service\n```\n\n**Result:**\n- 0 external dependencies (everything is stdlib)\n- 1 process (one binary does HTTP + workers + state + WebSocket)\n- Instant startup (Chez native code, no interpreter warmup)\n- 20-40MB RAM (no GC pressure from boxed JVM objects)\n- Static binary: 15MB\n- `scp edge user@server: && ssh server ./edge` to deploy\n- ~380 lines, one file, zero config files\n\n### Side-by-Side: Processing a Webhook\n\n**Clojure:**\n```clojure\n;; deps: ring, compojure, cheshire, core.async, carmine (redis)\n\n(ns webhook.handler\n  (:require [compojure.core :refer [defroutes POST]]\n            [ring.middleware.json :refer [wrap-json-body]]\n            [cheshire.core :as json]\n            [clojure.core.async :as async]\n            [taoensso.carmine :as redis]))\n\n(defroutes app-routes\n  (POST \"/hooks/:type\" [type :as req]\n    (let [payload (:body req)]\n      ;; Push to Redis queue (external process!)\n      (redis/wcar {} (redis/lpush \"webhooks\" (json/encode payload)))\n      {:status 202\n       :body (json/encode {:status \"queued\"})})))\n\n;; Separate worker process reads from Redis:\n(defn worker []\n  (loop []\n    (let [raw (redis/wcar {} (redis/brpop \"webhooks\" 0))\n          event (json/decode (second raw) true)]\n      (process-event! event)\n      (recur))))\n```\n\n**Jerboa:**\n```scheme\n;; deps: none (everything is stdlib)\n\n(import (jerboa prelude)\n        (std clojure)\n        (std net fiber-httpd)\n        (std csp clj)\n        (std text json))\n\n(def work-ch (chan 4096))\n\n(def (handle-webhook req)\n  (let ([payload (string->json-object (request-body req))])\n    (>!! work-ch payload)\n    (respond-json 202 \"{\\\"status\\\":\\\"queued\\\"}\")))\n\n;; Workers are fibers on the same process \u2014 no Redis, no IPC\n(dotimes (i 4)\n  (go (let loop ()\n        (let ([event (<!! work-ch)])\n          (when event\n            (process-event! event)\n            (loop))))))\n\n(fiber-httpd-start 8080 handle-webhook)\n```\n\nThe Jerboa version is **12 lines** vs Clojure's **20+ lines + Redis + a separate\nworker process**.  And the Jerboa version handles more concurrent connections\n(fibers vs threads), has built-in fault tolerance (supervisor), and deploys as\none file.\n\n---\n\n## Implementation Plan\n\n### Phase 1: Core Demo (the ~380-line file) \u2014 COMPLETE\n\n**Deliverable:** [`jerboa-edge/edge.ss`](https://git.sr.ht/~lisp/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa-edge\ncd jerboa-edge\nmake run\n\n# In another terminal:\ncurl -X POST localhost:8080/hooks/payment.completed \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"evt_123\",\"amount\":4999}'\n\ncurl localhost:8080/api/stats\n# => {\"total\":1,\"ok\":1,\"error\":0}\n```\n\n**Scope:**\n\n- [x] 1.1 **HTTP server with routing** \u2014 fiber-httpd with `:param` pattern routing.\n    Five endpoints: `POST /hooks/:type`, `GET /api/events/:id`, `GET /api/stats`,\n    `GET /health`, `GET /dashboard`.\n\n- [x] 1.2 **Channel-based ingestion** \u2014 CSP channel with transducer pipeline\n    (validate + dedup-by-ID + normalize).  Decouples accept from process.\n\n- [x] 1.3 **Actor-supervised workers** \u2014 N worker actors under a one-for-one\n    supervisor.  Each pulls from the channel, processes, writes to state.\n    `POST /hooks/test.crash` deliberately crashes a worker to show supervisor restart.\n\n- [x] 1.4 **STM state store** \u2014 All state in `(make-ref (make-persistent-map))`.\n    Atomic updates via `dosync`/`alter` with `(std pmap)` persistent maps.\n    Immutable snapshots \u2014 safe for concurrent reads during writes.\n\n- [x] 1.5 **WebSocket dashboard** \u2014 Single WS endpoint that streams\n    event-processed notifications to connected clients.  Sends initial stats\n    on connect.\n\n- [x] 1.6 **HMAC signature verification** \u2014 Validate `X-Signature` header using\n    `(std crypto native)` HMAC-SHA256.  Constant-time comparison.\n    Configurable via `EDGE_SECRET` env var.\n\n**Non-goals for Phase 1:**\n- Persistence across restarts (in-memory only)\n- TLS termination (use a reverse proxy, or wait for Phase 3)\n- Static binary packaging (Phase 3)\n\n**Success criteria:**\n- [x] Runs with `make run` (382 lines of Scheme)\n- [x] Handles 10K concurrent webhook POSTs without dropped events (verified: 9,993/10,000 accepted at 200 concurrent connections, ~454 req/sec)\n- [x] Worker crash + supervisor restart visible in logs\n- [x] WebSocket dashboard receives live event stream (verified with websocket-client)\n- [x] `curl` examples work out of the box (17/17 smoke tests pass)\n- [x] HMAC: no-sig and wrong-sig return 401, valid-sig returns 202\n\n### Phase 2: Programmability\n\n**Deliverable:** Sandboxed user filter support + handler registry\n\n- [x] 2.1 **Sandboxed eval** \u2014 Users submit filter expressions via\n    `POST /api/filters`.  Filters evaluated per-event via\n    `restricted-eval-string` (allowlist-only: no I/O, no FFI) with a\n    2-second engine-based timeout (no fork \u2014 fiber-safe).  Events that\n    fail any filter are dropped silently before processing.\n\n    ```\n    POST /api/filters  {\"name\":\"payments-only\",\"code\":\"(equal? (cdr (assoc \\\"type\\\" evt)) \\\"payment.completed\\\")\"}\n    GET  /api/filters\n    DELETE /api/filters/:name\n    ```\n\n    Key implementation detail: event data is bound as `evt` (a deep\n    alist), so filter code uses `assoc` with string keys (not `assq`).\n\n- [x] 2.2 **Hot handler registration** \u2014 New webhook types registered at\n    runtime via `POST /api/handlers`.  No restart required.  Handler\n    code runs in a custom restricted environment (`safe-bindings` +\n    `displayln`) with a 5-second engine timeout.  Exceptions propagate\n    to the worker's retry machinery.\n\n    ```\n    POST /api/handlers  {\"type\":\"order.shipped\",\"code\":\"(displayln \\\"shipped: \\\" (cdr (assoc \\\"id\\\" evt)))\"}\n    GET  /api/handlers\n    ```\n\n- [x] 2.3 **Retry with exponential backoff** \u2014 Failed events re-enter the\n    channel with an incremented retry count.  Workers delay via\n    `fork-thread` + `sleep` (non-blocking, OS-thread safe).  Dedup\n    transducer passes retried events (`retry-count > 0`).  After 3\n    retries, events go to a dead-letter STM store.\n\n    ```\n    GET  /api/dead-letter\n    POST /api/dead-letter/:id/replay\n    ```\n\n    Replay sets `retry-count: 1` to bypass the dedup filter.\n\n**Success criteria:**\n- [x] Filter code cannot call `open-file`, `socket`, or any I/O (allowlist env)\n- [x] Filter with 2s timeout verified: `with-timeout` uses Chez engines, no fork\n- [x] Hot-registered handlers process events without service restart (verified: `order.shipped`, `inventory.updated`)\n- [x] Failed events retry 3\u00d7 with 1s/2s/4s backoff then dead-letter (verified in log)\n- [x] Dead-letter replay re-processes event and clears queue (verified: status \u2192 \"ok\")\n- [x] Runs with `make run` (690 lines of Scheme)\n\n### Phase 3: Production Hardening \u2713\n\n**Deliverable:** Static binary, TLS, persistence, metrics (~1048 lines)\n\n- [x] 3.1 **Static musl binary** \u2014 `make edge-static` produces a fully static\n    ELF via `docker run jerboa21/jerboa` (musl Chez + Rust toolchain + WPO).\n    **Verified: 12MB, zero runtime dependencies.**  All native FFI symbols\n    registered via `Sforeign_symbol`; `JERBOA_STATIC=1` auto-set at startup\n    to bypass `load-shared-object` calls.  Deploy with `scp edge host:/usr/local/bin/`.\n\n- [x] 3.2 **TLS termination** \u2014 Optional `EDGE_TLS_CERT` / `EDGE_TLS_KEY`\n    env vars for direct HTTPS.  Uses `(std net tls-rustls)` \u2014 the Rust\n    rustls backend.  `make run-tls` target.  One OS thread per TLS connection;\n    same router handler for both plain and TLS paths.\n\n- [x] 3.3 **SQLite persistence** \u2014 `EDGE_DB_PATH` env var enables persistence\n    via `(std db sqlite-native)` (bundled rusqlite, no external libsqlite3).\n    WAL mode for concurrent reads.  Events and dead-letters loaded into STM\n    on startup.  All writes are fire-and-forget (guarded; SQLite error never\n    crashes a worker).  `make run-db` target.\n\n- [x] 3.4 **Prometheus metrics** \u2014 `/metrics` endpoint in Prometheus text\n    format via `(std metrics)`.  Exposed counters and histogram:\n    - `edge_events_received_total`\n    - `edge_events_ok_total`\n    - `edge_events_error_total`\n    - `edge_events_dead_letter_total`\n    - `edge_worker_restarts_total`\n    - `edge_processing_duration_seconds` (histogram, default buckets)\n    - `edge_active_connections` (gauge)\n\n- [x] 3.5 **Structured JSON logging** \u2014 All event-path log lines are JSON\n    (`jlog` helper, `json-object->string`).  Fields: `ts`, `msg`, plus\n    context fields (`worker`, `id`, `type`, `status`, `ms`, `err`).\n    Startup banner stays human-readable.\n\n- [x] 3.6 **Graceful shutdown** \u2014 `SIGTERM` triggers via `add-signal-handler!`\n    from `(std os signal)`.  Sequence: `fiber-httpd-stop!` \u2192 `chan-close!`\n    (workers see EOF) \u2192 3s drain window \u2192 `PRAGMA wal_checkpoint(FULL)` \u2192\n    `(exit 0)`.  Verified: completes in ~3s.\n\n**Success criteria:**\n- [x] `make edge-static` builds and runs \u2014 12MB, fully static, verified functional\n- [x] Graceful shutdown completes in ~3 seconds (verified)\n- [x] Prometheus scrape at `/metrics` returns well-formed text format (verified)\n- [x] SQLite events survive restart: `total:2 ok:2` after reload (verified)\n- [x] JSON log lines include worker, event id, type, status, duration (verified)\n- [x] Runs with `make run` (~1048 lines of Scheme)\n\n### Phase 4: Distribution \u2713\n\n**Deliverable:** Multi-node webhook processing via authenticated TCP transport and in-process Raft (~1244 lines)\n\n- [x] 4.1 **Clustered actors via `(std actor transport)`** \u2014 When\n    `EDGE_CLUSTER_COOKIE` is set, each node registers itself with\n    `start-node!` and starts a TCP server with `start-node-server!`.\n    Transport uses HMAC-SHA256 4-step challenge-response authentication\n    with per-message integrity (derived session key).  Each node spawns\n    a named `'edge-remote-ingest` actor that receives `(work event)`\n    messages from peers and enqueues them into the local ingest channel.\n    After HTTP starts, `connect-to-peers!` calls `GET /api/cluster/info`\n    on each peer to discover actor IDs, then builds remote refs via\n    `make-remote-actor-ref`.  Incoming webhooks are round-robined across\n    self + connected peers; failed peer sends fall back to local.\n    Wire format: `[4-byte BE length][fasl body][32-byte HMAC-SHA256]`.\n\n    ```bash\n    # Terminal 1 \u2014 node A (HTTP :8080, transport :9000)\n    make run-cluster-a\n\n    # Terminal 2 \u2014 node B (HTTP :8081, transport :9001)\n    make run-cluster-b\n\n    # POST to either node; events processed on either worker pool\n    curl -X POST localhost:8080/hooks/payment.completed -d '{\"id\":\"e1\",...}'\n    curl localhost:8080/api/cluster/info   # shows node_id, actor_id, peers\n    ```\n\n- [x] 4.2 **Replicated event ordering via `(std raft)`** \u2014 When\n    `EDGE_RAFT_SIZE > 0`, an N-node in-process Raft cluster runs alongside\n    the service.  Every accepted webhook is proposed to the Raft commit\n    log by the current leader (`raft-propose!`), providing a consensus-\n    ordered event sequence.  The commit log accumulates in-memory and is\n    available via `raft-log`/`raft-commit-index` on the leader node.\n\n    Note: `(std raft)` uses in-memory channels (no TCP) \u2014 it is an\n    in-process simulation designed to be bridged over a transport layer\n    for true cross-process Raft.  `(std crdt)` does not exist in the\n    stdlib; per-node STM with coordinator routing is used instead.\n\n    Note: `(std raft)` uses in-memory channels \u2014 cross-process Raft\n    would require bridging node inboxes over `(std actor transport)`.\n    `(std crdt)` does not exist in the stdlib.\n\n    ```bash\n    make run-raft RAFT_SIZE=3\n    curl localhost:8080/api/cluster/info\n    # => {\"raft_nodes\":3,\"raft_leader\":\"yes\",\"raft_term\":3,\"raft_commit\":5,...}\n    ```\n\n- [x] 4.3 **`GET /api/cluster/info`** \u2014 New endpoint returns full cluster\n    state: `node_id`, `ingest_actor_id`, `cluster_enabled`, `peers_connected`,\n    `raft_nodes`, `raft_leader`, `raft_term`, `raft_commit`.  Used by\n    peers at startup to discover actor IDs for remote refs.\n\n- [x] 4.4 **Transport shutdown** \u2014 `graceful-shutdown!` calls\n    `transport-shutdown!` (closes all authenticated TCP connections) and\n    `raft-stop!` on each Raft node before exit.\n\n**Success criteria:**\n- [x] `EDGE_CLUSTER_COOKIE=x make run` starts transport server on port 9000\n- [x] `make run-raft RAFT_SIZE=3` shows \"leader: elected, term: 3\" in logs\n- [x] `/api/cluster/info` returns raft_leader, raft_term, raft_commit (verified)\n- [x] Webhook processed correctly with Raft enabled (verified: status \"ok\")\n- [x] Graceful shutdown closes transport + stops Raft nodes\n- [x] Runs with `make run` (~1244 lines of Scheme)\n\n---\n\n## Module Dependency Map\n\nOnly stdlib modules \u2014 zero external dependencies:\n\n```\nexamples/webhook-service.ss\n\u251c\u2500\u2500 (jerboa prelude)              ;; base language\n\u251c\u2500\u2500 (std clojure)                 ;; polymorphic ops, inc/dec, etc.\n\u251c\u2500\u2500 (std component)               ;; lifecycle management\n\u251c\u2500\u2500 (std net fiber-httpd)         ;; HTTP server (fiber-native)\n\u2502   \u2514\u2500\u2500 (std net io)              ;; epoll-backed fiber I/O\n\u251c\u2500\u2500 (std net fiber-ws)            ;; WebSocket (fiber-native)\n\u251c\u2500\u2500 (std net router)              ;; URL routing with params\n\u251c\u2500\u2500 (std csp clj)                 ;; Clojure-style channels\n\u2502   \u251c\u2500\u2500 (std csp)                 ;; base CSP\n\u2502   \u251c\u2500\u2500 (std csp select)          ;; alts!/timeout\n\u2502   \u251c\u2500\u2500 (std csp fiber-chan)      ;; fiber-aware channels\n\u2502   \u2514\u2500\u2500 (std fiber)               ;; M:N fiber runtime\n\u251c\u2500\u2500 (std actor)                   ;; actors + supervision\n\u251c\u2500\u2500 (std stm)                     ;; transactional memory\n\u251c\u2500\u2500 (std transducer)              ;; composable transforms\n\u251c\u2500\u2500 (std text json)               ;; JSON parser/writer\n\u251c\u2500\u2500 (std crypto hmac)             ;; HMAC-SHA256 verification\n\u251c\u2500\u2500 (std security sandbox)        ;; Landlock/seccomp eval\n\u2514\u2500\u2500 (std pmap)                    ;; persistent hash maps\n```\n\nAll of these ship with Jerboa.  `(std crypto hmac)` and\n`(std security sandbox)` use the Rust native backend; everything else is\npure Scheme.\n\n---\n\n## Benchmarking Targets\n\nThese numbers establish what \"production-grade\" means for the demo:\n\n| Metric | Target | How to measure |\n|---|---|---|\n| Webhook throughput | 50K events/sec sustained | `wrk -c 1000 -t 8 -d 30s` |\n| p99 accept latency | < 5ms | Measure time from connect to 202 |\n| Memory (100K queued) | < 100MB | `/proc/self/status` VmRSS |\n| Worker restart time | < 50ms | Log timestamp delta after kill |\n| WebSocket fan-out | 1000 clients, < 10ms broadcast | Measure from STM commit to WS send |\n| Cold start | < 100ms | Time from exec to first request served |\n| Binary size | < 25MB | `ls -la edge` |\n| Concurrent connections | 100K+ | `ulimit -n` + `wrk` |\n\n### Benchmark Script\n\n```bash\n#!/bin/bash\n# bench-edge.sh \u2014 run against a live edge instance\n\necho \"=== Throughput ===\"\nwrk -c 500 -t 4 -d 10s -s post.lua http://localhost:8080/hooks/test\n\necho \"=== Latency ===\"\nwrk -c 1 -t 1 -d 5s -s post.lua --latency http://localhost:8080/hooks/test\n\necho \"=== Memory ===\"\nps -o rss= -p $(pgrep -f webhook-service) | awk '{print $1/1024 \"MB\"}'\n\necho \"=== Stats ===\"\ncurl -s localhost:8080/api/stats | python3 -m json.tool\n```\n\n---\n\n## What This Proves\n\nWhen someone sees this demo and asks \"why not just use Clojure?\", the answer\nis concrete:\n\n1. **Deploy in 5 seconds, not 5 minutes.** One binary, scp, run.  No JVM, no\n   Docker, no Redis, no config files.\n\n2. **20MB of RAM, not 2GB.**  Fibers are 4KB each.  Persistent maps share\n   structure.  No JVM heap overhead, no Redis process.\n\n3. **100ms cold start, not 30 seconds.**  Chez compiles to native code.  No\n   classloading, no dependency injection framework initialization.\n\n4. **Fault tolerance is built in, not bolted on.**  Actor supervision trees\n   restart crashed workers.  STM prevents data races.  You don't need to\n   add \"resilience4j\" as a dependency.\n\n5. **Security is built in, not bolted on.**  Sandboxed eval uses kernel-level\n   isolation (Landlock/seccomp).  No \"eval is dangerous\" disclaimer \u2014 eval\n   is sandboxed by default.\n\n6. **One file, ~1244 lines.**  Phase 1 was ~380 lines.  Phase 4 adds TLS,\n   Prometheus metrics, SQLite persistence, graceful shutdown, distributed\n   actor transport with HMAC-SHA256 auth, and in-process Raft consensus \u2014\n   and the result is still one file a senior engineer can read in an\n   afternoon.  Try that with a Spring Boot webhook processor.\n\nThis isn't a toy.  It handles 50K events/sec, restarts crashed workers in\n50ms, streams live updates over WebSocket, scrapes cleanly into Prometheus,\nand the entire thing fits in a single file with zero external dependencies.\n\n---\n\n## Related Work\n\n- `docs/green-wins.md` \u2014 fiber-httpd production hardening roadmap\n- `docs/clojure-left.md` \u2014 Clojure gap analysis and priority roadmap\n- `docs/clojure-vs-jerboa.md` \u2014 feature parity comparison\n- `examples/hello-api.ss` \u2014 simple HTTP API example (starting point)\n- `examples/chat-server.ss` \u2014 WebSocket + concurrency example\n- `lib/std/component.sls` \u2014 Stuart Sierra lifecycle (used in Phase 1)\n"}
 {"text": "<!-- FILE: docs/concurrency-extended.md -->\n# Concurrency & Control Flow Extensions\n\nAdvanced concurrency primitives, resource management, and control flow operators\nfor Jerboa's standard library.\n\n## Table of Contents\n\n- [Event System (`std misc event`)](#event-system)\n- [Custodians (`std misc custodian`)](#custodians)\n- [Resource Pool (`std misc pool`)](#resource-pool)\n- [Delimited Continuations (`std misc delimited`)](#delimited-continuations)\n- [Continuation Marks (`std misc cont-marks`)](#continuation-marks)\n- [Non-deterministic Backtracking (`std misc amb`)](#non-deterministic-backtracking)\n\n---\n\n## Event System\n\n**Module:** `(std misc event)`\n**File:** `lib/std/misc/event.sls`\n\n```scheme\n(import (std misc event))\n```\n\n### Overview\n\nEvents are lazy values that may or may not be ready. They can be composed with\n`choice`, transformed with `wrap`/`handle`, and synchronized with `sync` and\n`sync/timeout`. Channels provide synchronous rendezvous-style communication\nbetween threads, built on events.\n\nThe design follows the Concurrent ML (CML) model: events are first-class values\nthat represent potential communications. They are not \"fired\" until synchronized.\n\n### API Reference\n\n| Procedure | Signature | Description |\n|-----------|-----------|-------------|\n| `make-event` | `(make-event poll-thunk)` | Create an event from a poll thunk. The thunk must return `(values ready? value)`. |\n| `event-ready?` | `(event-ready? evt)` | Poll the event once; returns `#t` if it is currently ready. |\n| `event-value` | `(event-value evt)` | Block (spin-wait with backoff) until the event fires, then return its value. |\n| `sync` | `(sync evt ...)` | Wait for any one of the given events to fire. Returns the value of the first ready event. Blocks indefinitely. |\n| `sync/timeout` | `(sync/timeout timeout-ms evt ...)` | Like `sync` but returns `#f` if no event fires within `timeout-ms` milliseconds. |\n| `choice` | `(choice evt ...)` | Combine multiple events into one. When polled, tries each in order and returns the first ready value. |\n| `wrap` | `(wrap evt proc)` | Transform an event's value: when `evt` fires with value `v`, the wrapped event fires with `(proc v)`. |\n| `handle` | `(handle evt proc)` | Alias for `wrap`. |\n| `always-event` | `(always-event val)` | An event that is immediately ready with `val`. |\n| `never-event` | `never-event` | A value (not a procedure). An event that is never ready. |\n| `timer-event` | `(timer-event delay-ms)` | An event that fires with `#t` after `delay-ms` milliseconds. |\n| `make-channel` | `(make-channel)` | Create a synchronous rendezvous channel. |\n| `channel-send` | `(channel-send ch val)` | Blocking send: deposits `val` and waits until a receiver consumes it. |\n| `channel-recv` | `(channel-recv ch)` | Blocking receive: waits for a sender and returns the sent value. |\n| `channel-send-event` | `(channel-send-event ch val)` | An event for sending `val`. Fires (with `(void)`) when a receiver is waiting. |\n| `channel-recv-event` | `(channel-recv-event ch)` | An event that fires with the received value when one is available. |\n\n### Examples\n\n**Basic event polling:**\n\n```scheme\n(import (std misc event))\n\n;; An event that is always ready\n(let ([e (always-event 42)])\n  (event-ready? e))       ; => #t\n  (event-value e))        ; => 42\n\n;; never-event is never ready\n(event-ready? never-event) ; => #f\n```\n\n**Timer with sync/timeout:**\n\n```scheme\n;; Wait up to 500ms for a timer that fires at 100ms\n(sync/timeout 500 (timer-event 100))  ; => #t (the timer's value)\n\n;; Timeout expires before the timer fires\n(sync/timeout 10 (timer-event 5000))  ; => #f\n```\n\n**Choosing between events:**\n\n```scheme\n;; First-ready-wins: timer vs. channel receive\n(let ([ch (make-channel)])\n  ;; In another thread: (channel-send ch \"hello\")\n  (sync\n    (wrap (timer-event 1000) (lambda (v) 'timeout))\n    (wrap (channel-recv-event ch) (lambda (msg) (list 'got msg)))))\n;; => 'timeout if nothing sent within 1s, or '(got \"hello\") if sent\n```\n\n**Rendezvous channels between threads:**\n\n```scheme\n(import (std misc event) (chezscheme))\n\n(let ([ch (make-channel)])\n  ;; Producer thread\n  (fork-thread\n    (lambda ()\n      (channel-send ch \"hello\")\n      (channel-send ch \"world\")))\n  ;; Consumer\n  (let ([a (channel-recv ch)]\n        [b (channel-recv ch)])\n    (list a b)))\n;; => (\"hello\" \"world\")\n```\n\n**Custom event from a poll thunk:**\n\n```scheme\n;; An event that fires when a file exists\n(define (file-exists-event path)\n  (make-event\n    (lambda ()\n      (if (file-exists? path)\n          (values #t path)\n          (values #f #f)))))\n\n(sync/timeout 5000 (file-exists-event \"/tmp/ready.flag\"))\n```\n\n---\n\n## Custodians\n\n**Module:** `(std misc custodian)`\n**File:** `lib/std/misc/custodian.sls`\n\n```scheme\n(import (std misc custodian))\n```\n\n### Overview\n\nCustodians are hierarchical resource groups inspired by Racket's custodian\nmodel. Every managed resource (ports, handles, custom objects) belongs to a\ncustodian. Shutting down a custodian recursively shuts down all its children and\nreleases all their resources. The `with-custodian` form provides automatic\ncleanup on normal exit, exceptions, or continuation escapes.\n\n### API Reference\n\n| Procedure / Syntax | Signature | Description |\n|--------------------|-----------|-------------|\n| `make-custodian` | `(make-custodian)` or `(make-custodian parent)` | Create a child custodian. Defaults to `(current-custodian)` as parent. |\n| `custodian?` | `(custodian? x)` | Returns `#t` if `x` is a custodian. |\n| `current-custodian` | `(current-custodian)` | Parameter holding the current custodian. |\n| `custodian-register!` | `(custodian-register! resource shutdown-proc)` or `(custodian-register! custodian resource shutdown-proc)` | Register a resource with a shutdown thunk. Returns the resource. If custodian is omitted, uses `(current-custodian)`. |\n| `custodian-shutdown-all` | `(custodian-shutdown-all c)` | Recursively shut down custodian `c`: close all resources, shut down all children, remove from parent. Errors during individual resource cleanup are swallowed so one failure does not prevent others from cleaning up. |\n| `custodian-managed-list` | `(custodian-managed-list c)` | Return a list of all managed resources and child custodians for `c`. |\n| `custodian-open-input-file` | `(custodian-open-input-file path)` or `(custodian-open-input-file path custodian)` | Open an input port registered with the custodian for automatic cleanup. |\n| `custodian-open-output-file` | `(custodian-open-output-file path)` or `(custodian-open-output-file path custodian)` | Open an output port registered with the custodian for automatic cleanup. |\n| `with-custodian` | `(with-custodian body ...)` | Run `body ...` under a fresh custodian. The custodian is shut down when the body exits (normally, by exception, or by continuation escape). |\n\n### Examples\n\n**Automatic cleanup with `with-custodian`:**\n\n```scheme\n(import (std misc custodian))\n\n(with-custodian\n  (let ([p (custodian-open-input-file \"data.txt\")])\n    (read p)))\n;; Port is automatically closed when with-custodian exits\n```\n\n**Hierarchical resource management:**\n\n```scheme\n(let ([parent (make-custodian)])\n  (parameterize ([current-custodian parent])\n    (let ([child (make-custodian)])\n      (parameterize ([current-custodian child])\n        ;; Register resources under the child custodian\n        (let ([handle (list 'connection)])\n          (custodian-register! handle (lambda () (display \"closed\\n\")))))\n      ;; Inspect what the parent manages\n      (custodian-managed-list parent)))\n  ;; Shut down the parent \u2014 all children and their resources are cleaned up\n  (custodian-shutdown-all parent))\n```\n\n**Registering custom resources:**\n\n```scheme\n(with-custodian\n  ;; Register a custom handle with a cleanup procedure\n  (let ([sock (open-tcp-connection \"example.com\" 80)])\n    (custodian-register! sock (lambda () (close-port sock)))\n    (put-bytevector sock #vu8(71 69 84))\n    (get-bytevector-all sock)))\n;; sock is closed automatically on exit\n```\n\n---\n\n## Resource Pool\n\n**Module:** `(std misc pool)`\n**File:** `lib/std/misc/pool.sls`\n\n```scheme\n(import (std misc pool))\n```\n\n### Overview\n\nA thread-safe, generic resource pool. Resources are created on demand up to a\nconfigurable maximum, reused when idle, and optionally evicted after an idle\ntimeout. The pool uses a mutex and condition variable internally, so `pool-acquire`\ncan block without busy-waiting when the pool is full.\n\n### API Reference\n\n| Procedure / Syntax | Signature | Description |\n|--------------------|-----------|-------------|\n| `make-pool` | `(make-pool creator destroyer max-size)` or `(make-pool creator destroyer max-size idle-timeout)` | Create a pool. `creator` is a thunk returning a new resource. `destroyer` takes a resource and frees it. `max-size` is the maximum total resources (idle + in-use). `idle-timeout` is `#f` (no expiry) or a number of seconds after which idle resources are destroyed. |\n| `pool?` | `(pool? x)` | Returns `#t` if `x` is a pool. |\n| `pool-acquire` | `(pool-acquire p)` or `(pool-acquire p timeout)` | Get a resource from the pool. Reuses an idle resource if available, creates a new one if below max, or blocks. `timeout` is `#f` (block forever) or seconds. Returns the resource, or `#f` on timeout. |\n| `pool-release` | `(pool-release p resource)` | Return a resource to the pool, making it available for others. |\n| `with-resource` | `(with-resource pool (var) body ...)` | Acquire a resource, bind it to `var`, evaluate `body ...`, and release the resource on exit (even if an exception is raised). Uses `dynamic-wind`. |\n| `pool-drain` | `(pool-drain p)` | Destroy all idle resources. In-use resources are not affected. |\n| `pool-stats` | `(pool-stats p)` | Returns an alist: `((total . N) (idle . N) (in-use . N))`. Also evicts expired idle resources before counting. |\n\n### Examples\n\n**Basic connection pool:**\n\n```scheme\n(import (std misc pool))\n\n(define db-pool\n  (make-pool\n    (lambda () (open-db-connection \"localhost:5432\"))  ; creator\n    (lambda (c) (close-db-connection c))               ; destroyer\n    10))                                                ; max 10 connections\n\n;; Acquire, use, release\n(let ([conn (pool-acquire db-pool)])\n  (query conn \"SELECT 1\")\n  (pool-release db-pool conn))\n```\n\n**Using `with-resource` for automatic release:**\n\n```scheme\n(with-resource db-pool (conn)\n  (query conn \"SELECT * FROM users WHERE id = 1\"))\n;; conn is released back to the pool even on error\n```\n\n**Acquire with timeout:**\n\n```scheme\n;; Wait at most 5 seconds for a resource\n(let ([conn (pool-acquire db-pool 5)])\n  (if conn\n      (begin (query conn \"SELECT 1\")\n             (pool-release db-pool conn))\n      (display \"pool exhausted, try again later\\n\")))\n```\n\n**Idle timeout for resource eviction:**\n\n```scheme\n;; Resources idle for more than 60 seconds are destroyed\n(define pool\n  (make-pool\n    (lambda () (make-fresh-resource))\n    (lambda (r) (destroy-resource r))\n    20     ; max-size\n    60))   ; idle-timeout in seconds\n```\n\n**Pool statistics:**\n\n```scheme\n(pool-stats db-pool)\n;; => ((total . 3) (idle . 1) (in-use . 2))\n```\n\n**Drain idle resources:**\n\n```scheme\n(pool-drain db-pool)\n;; All idle resources are destroyed; in-use resources are unaffected\n(pool-stats db-pool)\n;; => ((total . 2) (idle . 0) (in-use . 2))\n```\n\n---\n\n## Delimited Continuations\n\n**Module:** `(std misc delimited)`\n**File:** `lib/std/misc/delimited.sls`\n\n```scheme\n(import (std misc delimited))\n```\n\n### Overview\n\nProvides delimited continuations via `reset`/`shift` (Danvy and Filinski style)\nand a prompt-based API (`call-with-prompt`/`abort-to-prompt`).\n\n`reset` establishes a delimiter (prompt) around an expression. `shift` captures\nthe continuation up to the nearest enclosing `reset` as a procedure `k`. The\ncaptured continuation can be called zero, one, or multiple times.\n\nThe implementation uses the Filinski encoding on top of `call/cc`.\n\n### API Reference\n\n| Procedure / Syntax | Signature | Description |\n|--------------------|-----------|-------------|\n| `reset` | `(reset body ...)` | Establish a continuation delimiter. Returns the value of `body ...`, or the value passed to `shift` if `shift` does not invoke `k`. |\n| `shift` | `(shift k body ...)` | Capture the continuation up to the nearest `reset` as `k`, then evaluate `body ...`. If `k` is never called, the `reset` returns the result of `body ...`. |\n| `make-prompt-tag` | `(make-prompt-tag)` or `(make-prompt-tag name)` | Create a prompt tag for use with `call-with-prompt`. |\n| `call-with-prompt` | `(call-with-prompt tag thunk handler)` | Run `thunk` under a prompt identified by `tag`. If `abort-to-prompt` is called with the same `tag`, control returns to the prompt and the result is the value(s) passed to `abort-to-prompt`. |\n| `abort-to-prompt` | `(abort-to-prompt tag val ...)` | Abort to the nearest prompt matching `tag`, returning `val ...`. Raises an error if no matching prompt is found. |\n\n### Examples\n\n**Basic reset/shift:**\n\n```scheme\n(import (std misc delimited))\n\n;; shift captures the continuation (+ 1 []) up to reset\n(reset (+ 1 (shift k (k 10))))    ; => 11\n\n;; If k is not called, the reset returns the shift body's value\n(reset (+ 1 (shift k 42)))        ; => 42\n\n;; k can be called multiple times\n(reset (+ 1 (shift k (+ (k 10) (k 20)))))  ; => 32\n;; (k 10) => 11, (k 20) => 21, 11 + 21 = 32\n```\n\n**Building a list with shift:**\n\n```scheme\n;; Collect elements via shift\n(reset\n  (let ([x (shift k (cons 'a (k 'ignored)))])\n    (let ([y (shift k (cons 'b (k 'ignored)))])\n      '())))\n;; => (a b)\n```\n\n**Prompt-based abort:**\n\n```scheme\n(let ([tag (make-prompt-tag 'my-prompt)])\n  (call-with-prompt tag\n    (lambda ()\n      (+ 1 (abort-to-prompt tag 99)))\n    (lambda (v) v)))\n;; => 99\n```\n\n**Simulating exceptions with shift:**\n\n```scheme\n(define (try thunk handler)\n  (reset\n    (handler (shift k (k (thunk))))))\n\n;; Not really needed with Chez's guard, but shows the pattern\n```\n\n---\n\n## Continuation Marks\n\n**Module:** `(std misc cont-marks)`\n**File:** `lib/std/misc/cont-marks.sls`\n\n```scheme\n(import (std misc cont-marks))\n```\n\n### Overview\n\nContinuation marks let you attach key-value metadata to continuation frames.\nThis module re-exports Chez Scheme's native continuation mark support with\nSRFI 157 / Racket-compatible names. Continuation marks are useful for\nimplementing dynamic parameters, stack traces, profiling, and other\ncontext-passing patterns without modifying function signatures.\n\n**Important:** Chez Scheme uses `eq?` for key comparison. Use symbols or fixnums\nas keys. String or pair keys only work if the exact same object is used for\nboth setting and lookup.\n\n### API Reference\n\n| Procedure / Syntax | Signature | Description |\n|--------------------|-----------|-------------|\n| `with-continuation-mark` | `(with-continuation-mark key val body)` | Evaluate `body` in a context where the current continuation frame has `key` mapped to `val`. If a mark for `key` already exists on this frame, it is replaced. |\n| `current-continuation-marks` | `(current-continuation-marks)` | Capture the full set of continuation marks from the current continuation. |\n| `continuation-mark-set->list` | `(continuation-mark-set->list mark-set key)` | Extract all values for `key` from the mark set as a list, innermost first. |\n| `continuation-mark-set-first` | `(continuation-mark-set-first mark-set key)` | Return the first (innermost) value for `key` from the mark set, or `#f` if not found. `mark-set` can be `#f` to use the current continuation marks. |\n| `continuation-marks?` | `(continuation-marks? x)` | Returns `#t` if `x` is a continuation mark set. |\n| `call-with-immediate-continuation-mark` | `(call-with-immediate-continuation-mark key default proc)` | Call `proc` with the value of `key` in the immediately enclosing continuation frame, or `default` if no mark for `key` exists. Note the argument order: key, default, proc. |\n\n### Examples\n\n**Basic mark and lookup:**\n\n```scheme\n(import (std misc cont-marks))\n\n(with-continuation-mark 'key 'val\n  (continuation-mark-set->list\n    (current-continuation-marks)\n    'key))\n;; => (val)\n```\n\n**Nested marks accumulate:**\n\n```scheme\n(with-continuation-mark 'depth 0\n  (with-continuation-mark 'depth 1\n    (with-continuation-mark 'depth 2\n      (continuation-mark-set->list\n        (current-continuation-marks)\n        'depth))))\n;; => (2 1 0)\n```\n\n**Tail-call mark replacement:**\n\nWhen marks are set in tail position relative to an existing mark on the same\nframe, the old value is replaced rather than accumulated:\n\n```scheme\n(with-continuation-mark 'k 'a\n  (with-continuation-mark 'k 'b  ;; replaces 'a on the same frame\n    (continuation-mark-set-first #f 'k)))\n;; => b\n```\n\n**Implementing a call stack trace:**\n\n```scheme\n(define (traced name thunk)\n  (with-continuation-mark 'trace name\n    (thunk)))\n\n(define (current-trace)\n  (continuation-mark-set->list\n    (current-continuation-marks)\n    'trace))\n\n(traced 'foo\n  (lambda ()\n    (traced 'bar\n      (lambda ()\n        (current-trace)))))\n;; => (bar foo)\n```\n\n**Immediate continuation mark:**\n\n```scheme\n(with-continuation-mark 'ctx \"request-123\"\n  (call-with-immediate-continuation-mark 'ctx #f\n    (lambda (v) v)))\n;; => \"request-123\"\n```\n\n---\n\n## Non-deterministic Backtracking\n\n**Module:** `(std misc amb)`\n**File:** `lib/std/misc/amb.sls`\n\n```scheme\n(import (std misc amb))\n```\n\n### Overview\n\nThe `amb` operator implements McCarthy's ambiguous choice, enabling\nnon-deterministic programming with automatic backtracking. `amb` picks one of\nits alternatives; if a later assertion fails, execution backtracks to the most\nrecent `amb` and tries the next alternative. This is useful for constraint\nsolving, search problems, and logic programming.\n\n### API Reference\n\n| Procedure / Syntax | Signature | Description |\n|--------------------|-----------|-------------|\n| `amb` | `(amb expr ...)` | Choose one of the expressions. If the current choice leads to failure, backtrack and try the next. `(amb)` with no arguments is equivalent to `(amb-fail)`. `(amb x)` with a single argument returns `x` directly. |\n| `amb-fail` | `(amb-fail)` | Explicitly trigger backtracking. Raises an error if there are no remaining choice points. |\n| `amb-assert` | `(amb-assert condition)` | If `condition` is `#f`, call `(amb-fail)` to backtrack. |\n| `with-amb` | `(with-amb body ...)` | Run an amb computation. Returns the first successful result, or `#f` if no solution exists. |\n| `amb-collect` | `(amb-collect body ...)` | Run an amb computation and collect all successful results into a list. |\n\n### Examples\n\n**Finding a solution:**\n\n```scheme\n(import (std misc amb))\n\n(with-amb\n  (let ([x (amb 1 2 3 4 5)]\n        [y (amb 1 2 3 4 5)])\n    (amb-assert (= (+ x y) 7))\n    (cons x y)))\n;; => (2 . 5)\n```\n\n**Collecting all solutions:**\n\n```scheme\n(amb-collect\n  (let ([x (amb 1 2 3 4 5)]\n        [y (amb 1 2 3 4 5)])\n    (amb-assert (= (+ x y) 6))\n    (cons x y)))\n;; => ((1 . 5) (2 . 4) (3 . 3) (4 . 2) (5 . 1))\n```\n\n**No solution returns `#f`:**\n\n```scheme\n(with-amb\n  (let ([x (amb 1 2 3)])\n    (amb-assert (> x 10))\n    x))\n;; => #f\n```\n\n**Pythagorean triples:**\n\n```scheme\n(define (iota-from a b)\n  ;; Returns list (a a+1 ... b)\n  (if (> a b) '() (cons a (iota-from (+ a 1) b))))\n\n(amb-collect\n  (let* ([a (apply amb (iota-from 1 20))]\n         [b (apply amb (iota-from a 20))]\n         [c (apply amb (iota-from b 20))])\n    (amb-assert (= (+ (* a a) (* b b)) (* c c)))\n    (list a b c)))\n;; => ((3 4 5) (5 12 13) (6 8 10) (8 15 17) (9 12 15) (12 16 20))\n```\n\n**Map coloring (constraint satisfaction):**\n\n```scheme\n(with-amb\n  (let ([wa (amb 'red 'green 'blue)]\n        [nt (amb 'red 'green 'blue)]\n        [sa (amb 'red 'green 'blue)]\n        [q  (amb 'red 'green 'blue)]\n        [nsw (amb 'red 'green 'blue)]\n        [v  (amb 'red 'green 'blue)]\n        [t  (amb 'red 'green 'blue)])\n    ;; Adjacent regions must differ\n    (amb-assert (not (eq? wa nt)))\n    (amb-assert (not (eq? wa sa)))\n    (amb-assert (not (eq? nt sa)))\n    (amb-assert (not (eq? nt q)))\n    (amb-assert (not (eq? sa q)))\n    (amb-assert (not (eq? sa nsw)))\n    (amb-assert (not (eq? sa v)))\n    (amb-assert (not (eq? q nsw)))\n    (amb-assert (not (eq? nsw v)))\n    (list (cons 'WA wa) (cons 'NT nt) (cons 'SA sa)\n          (cons 'Q q) (cons 'NSW nsw) (cons 'V v) (cons 'T t))))\n;; => ((WA . red) (NT . green) (SA . blue) (Q . red) (NSW . red) (V . green) (T . red))\n```\n"}
 {"text": "<!-- FILE: docs/reader-syntax.md -->\n# Jerboa Reader Syntax\n\n_Source of truth: `lib/jerboa/reader.sls`. Last audited 2026-04-22._\n\nJerboa extends Chez's reader with Gerbil-style and Clojure-style surface\nsyntax. **Which reader runs matters**:\n\n| Entry path                                 | Reader used        | `[...]` means          |\n|--------------------------------------------|--------------------|------------------------|\n| `scheme --libdirs lib --script file.ss`    | Chez built-in      | same as `(...)`        |\n| Code read via `(jerboa-read port)` / embed | Jerboa custom      | `(list ...)` wrapper   |\n| `.ss` file starting with `#!cloj`          | Jerboa, cloj mode  | `(list ...)` wrapper   |\n\n**If you are writing everyday `.ss` files and running them with `scheme\n--script`, brackets are just parens** \u2014 use them as block delimiters\nexactly like Gerbil. The Clojure-vector interpretation only activates\nthrough the explicit `jerboa-read` entry point (used by sandboxed embed\nand tooling that re-reads source).\n\nThe table below lists every extension the Jerboa reader adds on top of\nChez. All of these are available when `jerboa-read` is active. A \"mode\"\ncolumn shows `default` (always), `cloj` (only after `#!cloj` or\n`(reader-cloj-mode #t)`), or `both`.\n\n## Extensions\n\n| Syntax                 | Desugars to                                    | Mode    | Example                                          |\n|------------------------|------------------------------------------------|---------|--------------------------------------------------|\n| `[a b c]`              | `(list a b c)`                                 | both\u2020   | `[1 2 3]` \u2192 `(list 1 2 3)`                       |\n| `{m obj a ...}`        | `(~ obj 'm a ...)`                             | default | `{push! stk 1}` \u2192 `(~ stk 'push! 1)`             |\n| `{k v k v ...}`        | `(plist->hash-table (list k v ...))`           | cloj    | `{:a 1 :b 2}` \u2192 hash-map literal                 |\n| `{}` (empty)           | `(make-hash-table)`                            | cloj    | empty hash map                                   |\n| `:pkg/mod`             | `(pkg mod)`                                    | default | `:std/sort` \u2192 `(std sort)`                       |\n| `:pkg/sub/mod`         | `(pkg sub mod)`                                | default | `:std/text/json` \u2192 `(std text json)`             |\n| `:name`                | keyword `#:name`                               | cloj    | `:foo` \u2192 keyword                                 |\n| `name:`                | keyword `#:name`                               | both    | `port: 8080` \u2192 `#:port 8080`                     |\n| `#{a b c}`             | `(hash-set a b c)`                             | both    | `#{1 2 3}` \u2192 set literal                         |\n| `#(a b c)`             | vector `#(a b c)`                              | default | standard vector                                  |\n| `#(+ % 1)`             | `(fn-literal (+ % 1))`                         | cloj    | anonymous fn \u2014 `%` / `%1` / `%2` are args        |\n| `#<<END\\n...\\nEND`     | string literal (heredoc)                       | both    | multi-line raw text, newlines preserved          |\n| `#r\"a\\b\"`              | string `\"a\\\\b\"` (raw \u2014 only `\\\"` escapes)      | both    | useful for regex patterns                        |\n| `@expr`                | `(deref expr)`                                 | both    | `@atom` \u2192 `(deref atom)`                         |\n| `'x` `` `x `` `,x` `,@x` | quote / quasiquote / unquote / -splicing     | both    | standard Scheme                                  |\n| `#!cloj` (top of file) | activates Clojure reader mode for the file     | marker  | affects remaining reads                          |\n| `nil`                  | `#f`                                           | cloj    | literal identifier becomes false                 |\n| `true` / `false`       | `#t` / `#f`                                    | cloj    | literal identifiers                              |\n\n\u2020 Brackets are parens (`=` `(...)`) under Chez's built-in reader, which\nis what runs `.ss` scripts invoked with `scheme --script`. Only the\nJerboa custom reader wraps brackets with `list`.\n\n## What the reader does **not** provide\n\nThe following look like they could be reader syntax but are **plain\nidentifiers** provided by the prelude \u2014 they are NOT reader rewrites:\n\n- `->` `->>` `as->` `some->` `cond->` \u2014 threading macros (syntactic\n  forms, handled by the expander not the reader)\n- `p.x` dot-access \u2014 handled by the `using` form, not the reader\n- `:expr pred?` type casts \u2014 a two-argument form `(: e p)` expanded by\n  the prelude; `:` is a normal identifier\n\nBecause these are expander/macro features, you can rebind or shadow\nthem; reader-level syntax cannot be shadowed.\n\n## Safety limits\n\nThe Jerboa reader enforces resource ceilings (configurable via\nparameters in `(jerboa reader)`):\n\n- `(*max-nesting-depth*)` \u2014 how deep forms can nest\n- `(*max-list-length*)` \u2014 elements per list\n- `(*max-symbol-length*)` \u2014 chars per symbol\n- `(*max-comment-nesting*)` \u2014 depth of `#| ... |#` block comments\n\nThese prevent untrusted input from exhausting resources during parse.\nChez's built-in reader has no such limits, so any `.ss` file loaded\nthrough stock `scheme` bypasses them.\n\n## Quick reference for LLMs\n\nIf you are generating Jerboa code:\n\n1. **Default assumption**: the user runs code with `scheme --script`,\n   so brackets behave as parens. Write `(let ([x 1]) body)` freely.\n2. **Method dispatch**: `{m obj arg ...}` always means `(~ obj 'm\n   arg ...)`. Use when you know `obj` has a method `m`.\n3. **Module paths**: prefer the explicit `(import (std text json))`\n   form in examples; `:std/text/json` is correct but looks unusual\n   outside of Gerbil-style code.\n4. **Heredocs**: `#<<END\\nmulti\\nline\\nEND` is the only way to embed\n   a string containing unescaped quotes and newlines.\n5. **Never** write `(fn-literal ...)` or `(deref ...)` directly \u2014\n   prefer `(lambda (...) ...)` and `(@ atom)` spellings for clarity\n   unless you're targeting Clojure-mode code.\n"}
 {"text": "<!-- FILE: docs/safety-guide.md -->\n# Jerboa Safety Guide\n\nPractical guide for writing secure Jerboa applications. Covers all safety\nfeatures as of 2026-03-21.\n\n---\n\n## 1. Quick Start: The Safe Prelude\n\nThe single most important line in any Jerboa application:\n\n```scheme\n(import (jerboa prelude safe))\n```\n\nThis gives you the full Jerboa standard library with safety turned on by\ndefault. Specifically, you get:\n\n- **Contract-checked stdlib** \u2014 SQLite, TCP, file I/O, and JSON functions\n  validate arguments before FFI calls and return structured error conditions\n- **Resource management** \u2014 `with-resource` for RAII-style cleanup\n- **Structured concurrency** \u2014 `with-task-scope` instead of raw `fork-thread`\n- **Timeouts** \u2014 `with-timeout` and a configurable `*default-timeout*`\n- **Safe serialization** \u2014 `safe-fasl-read`/`safe-fasl-write` that reject\n  procedures and enforce size limits\n- **Sandbox entry point** \u2014 `run-safe` and `run-safe-eval` for untrusted code\n- **Error conditions** \u2014 structured hierarchy instead of bare `(error ...)`\n\nWhat you do NOT get (intentionally):\n\n- `foreign-procedure`, `c-lambda`, `begin-foreign` \u2014 no raw FFI\n- `fork-thread` \u2014 use `with-task-scope` and `scope-spawn` instead\n- `eval` \u2014 use `run-safe-eval` for untrusted input\n\nIf you need raw FFI or unscoped threads, import `(jerboa prelude)` instead.\nBut the safe prelude is the recommended default for application code.\n\n---\n\n## 2. The `run-safe` Sandbox\n\n`run-safe` is the one-call entry point for running untrusted code with all\nprotections applied. It forks a child process, applies irreversible kernel\nprotections, runs your code, and returns the result to the parent.\n\n### Basic Usage\n\n```scheme\n(import (jerboa prelude safe))\n\n;; Run a thunk with default protections:\n;; - 30 second timeout\n;; - compute-only seccomp filter (blocks network, filesystem writes)\n;; - No Landlock rules (add your own for filesystem restriction)\n(run-safe (lambda () (+ 1 2)))  ;; => 3\n\n;; Evaluate a string in a restricted environment (113-binding allowlist):\n(run-safe-eval \"(map (lambda (x) (* x x)) '(1 2 3))\")  ;; => (1 4 9)\n```\n\n### Custom Configuration\n\n```scheme\n;; Allow I/O but restrict to 10 seconds:\n(run-safe (lambda () (process-data input))\n  (make-sandbox-config\n    'timeout 10\n    'seccomp 'io-only))\n\n;; Filesystem sandbox: read-only access to specific directories:\n(run-safe (lambda () (read-config))\n  (make-sandbox-config\n    'landlock (make-readonly-ruleset \"/etc\" \"/usr/lib\")\n    'seccomp 'io-only\n    'timeout 5))\n\n;; No timeout (for trusted but resource-limited code):\n(run-safe (lambda () (long-computation))\n  (make-sandbox-config 'timeout #f 'seccomp 'compute-only))\n```\n\n### Config Options\n\n| Key | Values | Default | Effect |\n|-----|--------|---------|--------|\n| `timeout` | seconds or `#f` | 30 | Engine-based preemptive timeout |\n| `seccomp` | `'compute-only`, `'io-only`, `'network-server`, filter, `#f` | `'compute-only` | Syscall filter (irreversible) |\n| `landlock` | ruleset or `#f` | `#f` | Filesystem access restriction (irreversible) |\n| `capabilities` | list or `'()` | `'()` | Runtime capability tokens |\n\n### Default Parameters\n\nYou can set global defaults instead of passing config every time:\n\n```scheme\n(parameterize ([*sandbox-timeout* 10]\n               [*sandbox-seccomp* 'io-only])\n  (run-safe (lambda () (do-work))))\n```\n\n### What It Protects Against\n\n- **Code injection**: `run-safe-eval` uses a 113-binding allowlist. `system`,\n  `eval`, `load`, `foreign-procedure` are not in the allowlist. No amount of\n  clever input can summon them.\n- **Filesystem escape**: Landlock restricts filesystem access at the kernel\n  level. Once installed, it cannot be undone from Scheme or C.\n- **Syscall abuse**: Seccomp BPF filters are kernel-enforced. Even arbitrary\n  code execution inside the sandbox cannot bypass them.\n- **Infinite loops**: Engine-based timeout preempts runaway Scheme code.\n- **Parent compromise**: All protections are applied in a forked child. The\n  parent process remains unrestricted.\n\n### What It Does NOT Protect Against\n\n- **Memory exhaustion**: A `(make-bytevector 1000000000)` inside the sandbox\n  can OOM the child before the timeout fires. There is no `setrlimit` yet.\n  For DoS protection, run sandboxed code in a resource-limited container.\n- **Blocked FFI calls**: The engine timeout preempts Scheme code but cannot\n  interrupt a blocking C/Rust FFI call. Use socket-level timeouts\n  (`SO_RCVTIMEO`) for network operations.\n- **Temp file race**: The child communicates results via `/tmp/jerboa-sandbox-*`\n  with a random suffix. A local attacker with `/tmp` access could potentially\n  interfere. This is a known issue.\n\n---\n\n## 3. Resource Management\n\n### `with-resource` \u2014 Guaranteed Cleanup\n\n`with-resource` guarantees that resources are cleaned up even when exceptions\noccur. It uses `dynamic-wind` for LIFO cleanup ordering.\n\n```scheme\n(import (jerboa prelude safe))\n\n;; Multiple resources \u2014 cleaned up in reverse order on any exit:\n(with-resource ([db (sqlite-open \"app.db\")]\n                [sock (tcp-connect \"api.example.com\" 443)]\n                [f (open-safe-input-file \"config.txt\")])\n  (let ([config (read f)])\n    (sqlite-exec db (format \"INSERT INTO log(msg) VALUES('~a')\" \"started\"))\n    (tcp-write-string sock \"GET / HTTP/1.1\\r\\n\\r\\n\")))\n;; db, sock, and f are ALL guaranteed closed here\n\n;; Single resource shorthand:\n(with-resource1 (db (sqlite-open \"test.db\"))\n  (sqlite-query db \"SELECT 1\"))\n```\n\n### Auto-Detection\n\n`with-resource` auto-detects cleanup procedures for common types:\n\n- **Ports** (files, string ports) \u2014 calls `close-port` (flushes output first)\n- **Registered types** \u2014 anything registered via `register-resource-cleanup!`\n\nFor other types, provide an explicit cleanup:\n\n```scheme\n(with-resource ([buf (make-bytevector 4096) (lambda (b) (bytevector-fill! b 0))])\n  (fill-buffer buf))\n```\n\n### Guardian Safety Net\n\nIf you forget `with-resource`, the guardian-based finalizer in `(std safe)`\ncatches resource handles that are garbage collected without being closed. It\nlogs a warning to `current-error-port` and attempts best-effort cleanup.\n\nThis is a safety net, not a strategy. Always use `with-resource` explicitly.\n\nTo check for leaked resources at shutdown:\n\n```scheme\n(poll-resource-finalizers!)\n```\n\n---\n\n## 4. Contract-Checked Standard Library\n\nWhen you import `(jerboa prelude safe)`, the standard names (`sqlite-exec`,\n`tcp-connect`, etc.) are transparently replaced with contract-checked\nversions from `(std safe)`.\n\n### What the Contracts Check\n\n- **Type validation** before FFI calls \u2014 e.g., `sqlite-exec` verifies the\n  database handle and SQL string before calling into C\n- **Post-condition checks** on results\n- **SQL injection heuristics** \u2014 multi-statement detection, comment injection,\n  and other patterns are rejected at runtime\n\n```scheme\n(import (jerboa prelude safe))\n\n;; This works \u2014 single statement, no injection patterns:\n(sqlite-exec db \"SELECT * FROM users WHERE id = ?\")\n\n;; This is rejected \u2014 multi-statement detected:\n(sqlite-exec db \"SELECT 1; DROP TABLE users\")\n;; => raises &db-query-error with SQL injection warning\n```\n\n### SQL Injection Detection Limits\n\nThe runtime SQL check is a heuristic speed bump, not a wall. It catches\ncommon patterns (`;` statement separators, `--` comments, `/* */` blocks)\nbut a determined adversary can craft bypasses. **Always use parameterized\nqueries as your primary defense**:\n\n```scheme\n;; GOOD \u2014 parameterized:\n(sqlite-query db \"SELECT * FROM users WHERE id = ?\" user-id)\n\n;; BAD \u2014 string interpolation (the lint rule will also warn):\n(sqlite-query db (string-append \"SELECT * FROM users WHERE id = \" user-id))\n```\n\n### Release Mode\n\nFor production code that has been tested, you can skip contract checks for\nzero overhead:\n\n```scheme\n(parameterize ([*safe-mode* 'release])\n  (sqlite-exec db \"SELECT 1\"))  ;; no pre/post-condition checks\n```\n\nDefault is `'check`. Only use `'release` after thorough testing.\n\n---\n\n## 5. Structured Concurrency\n\nThe safe prelude exports structured concurrency primitives and intentionally\ndoes NOT export `fork-thread`.\n\n### `with-task-scope`\n\nAll spawned tasks are guaranteed terminated when the scope exits. No task\noutlives its scope.\n\n```scheme\n(import (jerboa prelude safe))\n\n;; Spawn parallel work \u2014 both tasks are cancelled if scope exits early:\n(with-task-scope\n  (let ([t1 (scope-spawn (lambda () (fetch-url \"https://api.example.com/a\")))]\n        [t2 (scope-spawn (lambda () (query-db \"SELECT count(*) FROM users\")))])\n    (values (task-await t1) (task-await t2))))\n\n;; Named tasks (for debugging):\n(with-task-scope\n  (let ([t (scope-spawn-named \"data-loader\" (lambda () (load-data)))])\n    (task-await t)))\n```\n\n### `parallel` and `race`\n\nConvenience forms for common patterns:\n\n```scheme\n;; Run all, return all results:\n(parallel\n  (lambda () (fetch-users))\n  (lambda () (fetch-orders))\n  (lambda () (fetch-config)))\n\n;; Run all, return first to complete, cancel the rest:\n(race\n  (lambda () (query-primary-db \"SELECT 1\"))\n  (lambda () (query-replica-db \"SELECT 1\")))\n```\n\n### Task Control\n\n```scheme\n(with-task-scope\n  (let ([t (scope-spawn (lambda () (slow-computation)))])\n    ;; Check if done without blocking:\n    (task-done? t)    ;; => #f\n\n    ;; Cancel:\n    (task-cancel t)\n\n    ;; Block until result (or re-raise task exception):\n    (task-await t)))\n```\n\n### Why Not `fork-thread`?\n\nRaw `fork-thread` creates threads that outlive their calling context. This\nleads to:\n\n- Resource leaks when threads hold references to closed handles\n- Uncatchable exceptions in threads that nobody joins\n- Race conditions that are hard to reproduce\n\n`with-task-scope` prevents all of these by construction. If you genuinely\nneed raw threads, import `(jerboa prelude)` instead \u2014 but you lose the\nstructured cleanup guarantees.\n\n---\n\n## 6. Timeouts\n\n### `with-timeout`\n\nUses Chez Scheme's engine system for true preemptive timeout. Works even if\nthe code is stuck in a Scheme-level infinite loop.\n\n```scheme\n(import (jerboa prelude safe))\n\n;; Timeout after 5 seconds:\n(with-timeout 5\n  (tcp-read sock 1024))\n\n;; Raises &jerboa-timeout on expiry.\n;; Handle it:\n(guard (exn\n        [(timeout-error? exn) (displayln \"operation timed out\")])\n  (with-timeout 2\n    (infinite-loop)))\n```\n\n### `*default-timeout*`\n\nControls the default timeout for safe-* operations. Default is 30 seconds.\n\n```scheme\n;; Set a 10-second default for this scope:\n(parameterize ([*default-timeout* 10])\n  ;; All safe operations in this dynamic extent use 10s timeout\n  (tcp-read sock 1024))\n\n;; Disable timeouts (not recommended for untrusted input):\n(parameterize ([*default-timeout* #f])\n  (long-trusted-computation))\n```\n\n### Limitations\n\nThe engine-based timeout cannot interrupt blocked foreign calls (C/Rust FFI).\nIf a C function blocks on a socket read, the engine timer has no effect until\ncontrol returns to Scheme. For network I/O, set socket-level timeouts\n(`SO_RCVTIMEO`/`SO_SNDTIMEO`) as a complementary measure.\n\n---\n\n## 7. Type Checking\n\n### `define/ct` \u2014 Compile-Time Checked Functions\n\n```scheme\n(import (std typed check))\n\n(define/ct (add [x : integer] [y : integer]) : integer\n  (+ x y))\n\n(define/ct (greet [name : string]) : string\n  (string-append \"Hello, \" name))\n```\n\nType mismatches are caught at compile time (macro expansion time). By default\nthey are warnings. For strict enforcement:\n\n```scheme\n;; Make type mismatches fatal compilation errors:\n(parameterize ([*type-errors-fatal* #t])\n  (compile-file \"my-module.ss\"))\n```\n\n### `lambda/ct` \u2014 Typed Lambdas\n\n```scheme\n(define handler\n  (lambda/ct ([request : string] [timeout : integer]) : string\n    (process request timeout)))\n```\n\n### Checking Existing Code\n\n```scheme\n;; Type-check a file and get error reports:\n(type-check-file \"my-module.ss\")\n;; => list of type-error records\n\n;; Type-check forms programmatically:\n(check-program-types\n  '((define (f x) (+ x \"hello\"))))  ;; => type mismatch: integer vs string\n```\n\n### Coverage Limitations\n\nThe type system is gradual and opt-in. Only functions annotated with\n`define/ct` or `lambda/ct` are checked. Calls from checked code to unchecked\ncode cross an unchecked boundary \u2014 type confusion bugs can still occur there.\nThis is standard for gradual type systems.\n\n---\n\n## 8. Kernel Sandboxing\n\n### Landlock \u2014 Filesystem Restriction\n\nLandlock restricts filesystem access without root. Rules are irreversible.\nAvailable on Linux 5.13+.\n\n```scheme\n(import (std security landlock))\n\n;; Build a ruleset:\n(let ([rules (make-landlock-ruleset)])\n  ;; Grant read-only access to specific directories:\n  (landlock-add-read-only! rules \"/usr/lib\")\n  (landlock-add-read-only! rules \"/etc\")\n  ;; Grant read-write to a working directory:\n  (landlock-add-read-write! rules \"/tmp/myapp\")\n  ;; Grant execute permission:\n  (landlock-add-execute! rules \"/usr/bin\")\n  ;; Install (IRREVERSIBLE \u2014 cannot undo):\n  (landlock-install! rules))\n;; All filesystem access outside these paths is now blocked\n\n;; Convenience: scope-based Landlock\n(with-landlock (make-readonly-ruleset \"/usr/lib\" \"/etc\")\n  (read-config-files))\n\n;; Pre-built rulesets:\n(make-readonly-ruleset \"/path1\" \"/path2\" ...)\n(make-tmpdir-ruleset \"/tmp/myapp\")\n\n;; Check availability:\n(landlock-available?)  ;; => #t on Linux 5.13+ x86_64\n```\n\n### Seccomp \u2014 Syscall Filtering\n\nSeccomp BPF restricts which system calls a process can make. Filters are\nirreversible and kernel-enforced. Available on Linux x86_64.\n\n```scheme\n(import (std security seccomp))\n\n;; Pre-built filters:\n(seccomp-install! (compute-only-filter))    ;; CPU-only: blocks network, FS writes\n(seccomp-install! (io-only-filter))         ;; Allow file I/O, block network\n(seccomp-install! (network-server-filter))  ;; Allow network, block exec\n\n;; Custom filter:\n(let ([filter (make-seccomp-filter\n                'default-action (seccomp-errno 1)  ;; EPERM for unmatched\n                'allowed '(read write exit-group brk mmap mprotect))])\n  (seccomp-install! filter))\n\n;; Check availability:\n(seccomp-available?)  ;; => #t on Linux x86_64\n```\n\n### When to Use Directly vs. via `run-safe`\n\nUse `run-safe` when:\n- You want all protections applied together (timeout + seccomp + Landlock)\n- You need fork-based isolation (protections apply only to child)\n- You are running untrusted code\n\nUse Landlock/seccomp directly when:\n- You want to restrict the main process permanently (e.g., a server that\n  drops privileges after startup)\n- You need fine-grained control over the protection order\n- You want irreversible self-restriction as a defense-in-depth measure\n\nRemember: **both Landlock and seccomp are irreversible**. Once installed in a\nprocess, they cannot be removed \u2014 only tightened further. `run-safe` handles\nthis by forking a child, so the parent stays unrestricted.\n\n---\n\n## 9. Error Handling\n\n### Structured Condition Hierarchy\n\nAll Jerboa errors inherit from `&jerboa`, which inherits from R6RS `&serious`.\nUse `guard` to catch specific error kinds:\n\n```scheme\n(import (jerboa prelude safe))\n\n(guard (exn\n        [(db-query-error? exn)\n         (displayln \"bad query\")]\n        [(connection-refused? exn)\n         (displayln \"server down\")]\n        [(timeout-error? exn)\n         (displayln \"timed out\")]\n        [(resource-leak? exn)\n         (displayln \"resource leaked\")]\n        [(jerboa-condition? exn)\n         (displayln (format \"jerboa error in ~a\"\n                            (jerboa-condition-subsystem exn)))])\n  (do-work))\n```\n\n### Full Hierarchy\n\n```\n&jerboa (root)\n  &jerboa-network\n    &connection-refused\n    &connection-timeout\n    &dns-failure\n    &tls-error\n    &network-read-error\n    &network-write-error\n  &jerboa-db\n    &db-connection-error\n    &db-query-error\n    &db-constraint-violation\n    &db-timeout\n  &jerboa-actor\n    &actor-dead\n    &mailbox-full\n    &supervision-failure\n    &actor-timeout\n  &jerboa-resource\n    &resource-leak\n    &resource-already-closed\n    &resource-exhausted\n  &jerboa-timeout\n  &jerboa-serialization\n    &unsafe-deserialize\n    &serialize-size-exceeded\n  &jerboa-parse\n    &parse-depth-exceeded\n    &parse-size-exceeded\n    &parse-invalid-input\n```\n\n### Error Context\n\n`(std error context)` provides `with-context` for accumulating diagnostic\ncontext through call chains:\n\n```scheme\n(import (std error context))\n\n(with-context \"loading user profile\"\n  (with-context (format \"user-id: ~a\" user-id)\n    (load-profile user-id)))\n;; If load-profile raises, the error includes both context strings\n```\n\n---\n\n## 10. Safe Serialization\n\n### `safe-fasl-read` / `safe-fasl-write`\n\nWraps Chez FASL with safety checks for untrusted data:\n\n```scheme\n(import (jerboa prelude safe))\n\n;; Write (rejects procedures by default):\n(call-with-output-file \"data.fasl\"\n  (lambda (port) (safe-fasl-write '(1 2 3) port)))\n\n;; Read (rejects procedures and unregistered record types):\n(call-with-input-file \"data.fasl\"\n  (lambda (port) (safe-fasl-read port)))\n\n;; Bytevector convenience:\n(let ([bytes (safe-fasl-write-bytevector '(1 2 3))])\n  (safe-fasl-read-bytevector bytes))\n```\n\n### Configuration\n\n```scheme\n;; Allow procedures (for trusted local persistence only):\n(parameterize ([*fasl-allow-procedures* #t])\n  (safe-fasl-write my-closure port))\n\n;; Size limits:\n(parameterize ([*fasl-max-object-count* 10000]     ;; max 10K objects (default 1M)\n               [*fasl-max-byte-size* (* 1 1024 1024)])  ;; max 1MB (default 100MB)\n  (safe-fasl-read port))\n```\n\n### Record Type Registry\n\nOnly explicitly registered record types can be deserialized:\n\n```scheme\n(define-record-type user (fields name email))\n\n;; Register for deserialization:\n(register-safe-record-type! (record-type-descriptor user))\n\n;; Now safe-fasl-read will accept user records.\n;; Unregister when no longer needed:\n(unregister-safe-record-type! (record-type-descriptor user))\n```\n\n### Decompression Bomb Limits\n\nThese limits apply across the safe APIs:\n\n| Parser | Parameter | Default |\n|--------|-----------|---------|\n| JSON | `*json-max-depth*` | 512 |\n| JSON | `*json-max-string-length*` | 10 MB |\n| XML/SXML | `*sxml-max-depth*` | 512 |\n| XML/SXML | `*sxml-max-output-size*` | 50 MB |\n| FASL | `*fasl-max-object-count*` | 1,000,000 |\n| FASL | `*fasl-max-byte-size*` | 100 MB |\n| Zlib | (hardcoded in Rust backend) | 100 MB |\n\n---\n\n## 11. Linting\n\n`(std lint)` provides 14 static analysis rules. Run them on your source files\nto catch common issues before they become bugs.\n\n### Usage\n\n```scheme\n(import (std lint))\n\n;; Lint a file:\n(let ([results (lint-file default-linter \"my-module.ss\")])\n  (for-each\n    (lambda (r)\n      (displayln (format \"[~a] ~a: ~a\"\n                         (lint-result-severity r)\n                         (lint-result-rule r)\n                         (lint-result-message r))))\n    results))\n\n;; Lint a string:\n(lint-string default-linter \"(import (std db sqlite-native)) (define car 42)\")\n\n;; Summary counts:\n(lint-summary results)  ;; => ((error . 0) (warn . 2) (info . 1))\n```\n\n### The 14 Rules\n\n**Safety rules** (severity: warn):\n\n| Rule | Detects |\n|------|---------|\n| `unsafe-import` | Importing raw FFI modules when safe alternatives exist |\n| `sql-interpolation` | Building SQL via `string-append`/`format` inside sqlite calls |\n| `redefine-builtin` | Redefining `car`, `map`, `+`, etc. |\n| `duplicate-import` | Importing the same module more than once |\n\n**Safety rules** (severity: info):\n\n| Rule | Detects |\n|------|---------|\n| `bare-error` | Using `(error ...)` instead of structured conditions |\n| `unused-only-import` | Symbols imported via `(only ...)` that are never used |\n\n**Hygiene rules** (severity: warn/info):\n\n| Rule | Detects |\n|------|---------|\n| `shadowed-define` | Bindings that shadow outer bindings |\n| `unused-define` | Top-level definitions never referenced in the same file |\n\n**Style rules** (severity: info):\n\n| Rule | Detects |\n|------|---------|\n| `empty-begin` | `(begin)` with no forms |\n| `single-arm-cond` | `cond` with only one clause (use `if` instead) |\n| `missing-else` | `if` without an else branch |\n| `deep-nesting` | Expressions nested more than 10 levels |\n| `long-lambda` | Lambda bodies with more than 20 forms |\n| `magic-number` | Numeric literals > 100 (suggest named constants) |\n\n### Custom Rules\n\n```scheme\n(let ([linter (make-linter)])\n  ;; Add a custom rule:\n  (add-rule! linter 'no-display\n    (lambda (forms)\n      (filter-map\n        (lambda (f)\n          (and (pair? f) (eq? (car f) 'display)\n               (make-lint-result #f #f #f 'warn\n                 \"use displayln instead of display\" 'no-display)))\n        forms)))\n\n  ;; Remove a built-in rule:\n  (remove-rule! linter 'magic-number)\n\n  (lint-file linter \"my-module.ss\"))\n```\n\n---\n\n## 12. Known Limitations\n\nBe honest about what Jerboa does NOT protect against:\n\n### No Memory Limits in Sandbox\n\n`run-safe` does not set `setrlimit(RLIMIT_AS)` in the child process. A\nmalicious thunk can exhaust memory before the timeout fires. If you run\nuntrusted code, use OS-level memory limits (cgroups, containers) as a\ncomplementary measure.\n\n### Kernel Protections Are Linux x86_64 Only\n\nLandlock requires Linux 5.13+. Seccomp BPF uses x86_64-specific syscall\nnumbers. On other platforms (macOS, ARM Linux, BSDs), these protections\nsilently degrade \u2014 the code runs without kernel sandboxing. The Scheme-level\nallowlist and timeout still apply.\n\n### Temp File Race in `run-safe`\n\n`run-safe` communicates results via `/tmp/jerboa-sandbox-<random>`. A local\nattacker with `/tmp` access could create symlinks to redirect the result\nfile. This should use `mkstemp` or `O_TMPFILE` but does not yet.\n\n### Engine Timeout Cannot Interrupt FFI\n\nThe Chez engine timeout preempts Scheme computation but not blocked foreign\ncalls. If your code calls a C function that blocks on a socket, the timeout\nhas no effect until that call returns. Set socket-level timeouts separately.\n\n### Type System Is Gradual\n\nOnly `define/ct` and `lambda/ct` annotated functions are type-checked. Calls\nfrom checked code into unchecked code cross an unchecked boundary. Type\nconfusion bugs can occur at these boundaries.\n\n### No Race Detector\n\nThere is no dynamic race detection for concurrent code. Structured concurrency\nvia `with-task-scope` reduces risk but does not eliminate data races. Code\nusing raw `fork-thread` (from the non-safe prelude) has no race detection.\n\n### Deadlock Detection Is Opt-In\n\n`(std concur deadlock)` provides `make-checked-mutex` and\n`with-checked-mutex` for runtime deadlock detection via wait-for graph\nanalysis. This is not automatic \u2014 you must explicitly use checked mutexes.\nRegular `make-mutex` has no deadlock detection.\n\n### SQL Injection Heuristic Is Bypassable\n\nThe runtime SQL injection check in `(std safe)` uses pattern matching. It\ncatches common attacks but a determined adversary with knowledge of the\nheuristics can craft bypasses. Parameterized queries are the real defense.\n\n### Import Conflict in Safe Prelude\n\nThe safe prelude has overlapping symbol exports between imported modules that\nproduce a \"multiple definitions\" warning. This is cosmetic but means symbol\nresolution order could theoretically surprise you in edge cases. In practice,\nthe safe version always wins because it is bound last.\n\n### Build Reproducibility Is Partial\n\n`(std build reproducible)` provides content-addressed artifacts and SHA-256\nhashing, but bit-identical reproducible builds end-to-end are not yet\nverified. The SBOM detects Scheme, C, and Rust dependencies but full build\norchestration is not complete.\n\n---\n\n## Recommended Patterns\n\n### Server That Drops Privileges\n\n```scheme\n(import (jerboa prelude safe))\n(import (std security landlock))\n(import (std security seccomp))\n\n;; Phase 1: Start up with full privileges\n(define config (read-config \"/etc/myapp/config.scm\"))\n(define db (sqlite-open (config-db-path config)))\n(define server (tcp-listen (config-port config)))\n\n;; Phase 2: Drop privileges (irreversible)\n(let ([rules (make-landlock-ruleset)])\n  (landlock-add-read-only! rules \"/etc/myapp\")\n  (landlock-add-read-write! rules \"/var/lib/myapp\")\n  (landlock-install! rules))\n(seccomp-install! (network-server-filter))\n\n;; Phase 3: Serve requests with restricted privileges\n(let loop ()\n  (with-resource ([client (tcp-accept server)])\n    (with-timeout 30\n      (handle-request db client)))\n  (loop))\n```\n\n### Processing Untrusted User Input\n\n```scheme\n(import (jerboa prelude safe))\n\n(define (eval-user-expression user-input)\n  ;; Fork a child, apply all protections, evaluate in 113-binding allowlist:\n  (guard (exn\n          [(sandbox-error? exn)\n           (format \"Error: ~a\" (sandbox-error-detail exn))]\n          [(timeout-error? exn)\n           \"Timed out\"])\n    (run-safe-eval user-input\n      (make-sandbox-config 'timeout 5))))\n```\n\n### Robust Resource Handling\n\n```scheme\n(import (jerboa prelude safe))\n\n(define (transfer-data source-path dest-path)\n  (with-resource ([in (open-safe-input-file source-path)]\n                  [out (open-safe-output-file dest-path)])\n    (with-timeout 60\n      (let loop ()\n        (let ([line (get-line in)])\n          (unless (eof-object? line)\n            (put-string out line)\n            (newline out)\n            (loop)))))))\n;; Both files guaranteed closed, even on timeout or exception\n```\n\n### Parallel Work with Cancellation\n\n```scheme\n(import (jerboa prelude safe))\n\n(define (fetch-with-fallback primary-url fallback-url)\n  ;; First to complete wins, other is cancelled:\n  (race\n    (lambda () (fetch-url primary-url))\n    (lambda () (fetch-url fallback-url))))\n```\n"}
@@ -358,7 +358,7 @@
 {"text": "<!-- FILE: docs/wasm-example.md -->\n# WASM Practical Examples\n\nThis guide shows how to write real programs using Jerboa's WASM pipeline:\ncompile Scheme to WASM bytecode, run it in the Rust wasmi sandbox, and\nintegrate with jsh (Jerboa Shell).\n\nFor architecture and API reference, see [wasm.md](wasm.md).\n\n## Table of Contents\n\n- [Prerequisites](#prerequisites)\n- [Example 1: Numeric Computation](#example-1-numeric-computation)\n- [Example 2: Memory-Based Data Processing](#example-2-memory-based-data-processing)\n- [Example 3: A Byte-Level Tokenizer](#example-3-a-byte-level-tokenizer)\n- [Example 4: Parser with Import Callbacks](#example-4-parser-with-import-callbacks)\n- [Example 5: State Machine Protocol Parser](#example-5-state-machine-protocol-parser)\n- [Compiling to .wasm Files](#compiling-to-wasm-files)\n- [String I/O Conventions](#string-io-conventions)\n- [What Works Well in WASM](#what-works-well-in-wasm)\n- [Current Limitations](#current-limitations)\n- [jsh Integration Roadmap](#jsh-integration-roadmap)\n\n## Prerequisites\n\n```scheme\n(import (except (chezscheme) compile-program)\n        (jerboa wasm format)\n        (jerboa wasm codegen)\n        (jerboa wasm runtime))\n\n;; For Rust wasmi sandbox (optional \u2014 provides ROP isolation):\n(import (std wasm sandbox))\n```\n\nTwo execution modes:\n\n| Mode | Module | Runs in | Use case |\n|------|--------|---------|----------|\n| **Interpreter** | `(jerboa wasm runtime)` | Chez Scheme process | Development, debugging |\n| **Sandbox** | `(std wasm sandbox)` | Rust wasmi (separate address space) | Production, untrusted code |\n\nBoth accept the same bytecode from `compile-program`.\n\n---\n\n## Example 1: Numeric Computation\n\nPure functions on i32/i64/f32/f64 \u2014 the simplest case.\n\n```scheme\n(define bv (compile-program\n  '((define (factorial (n i32) -> i32)\n      (if (= n 0) 1 (* n (factorial (- n 1)))))\n\n    (define (fibonacci (n i32) -> i32)\n      (if (<= n 1) n\n        (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))\n\n    (define (gcd (a i32) (b i32) -> i32)\n      (if (= b 0) a (gcd b (remainder a b))))\n\n    (define (is-prime (n i32) -> i32)\n      (if (<= n 1) 0\n        (let ((i 2) (limit (+ 1 (i32.trunc_f64_s (f64.sqrt (f64.convert_i32_s n))))))\n          (while (and (<= i limit) (!= (remainder n i) 0))\n            (set! i (+ i 1)))\n          (if (> i limit) 1 0)))))))\n\n;; Run in interpreter\n(define rt (make-wasm-runtime))\n(wasm-runtime-load rt bv)\n(wasm-runtime-call rt \"factorial\" 12)   ;; => 479001600\n(wasm-runtime-call rt \"fibonacci\" 20)   ;; => 6765\n(wasm-runtime-call rt \"gcd\" 48 18)      ;; => 6\n(wasm-runtime-call rt \"is-prime\" 997)   ;; => 1\n\n;; Run in Rust wasmi sandbox (identical bytecode)\n(define mod-h (wasm-sandbox-load bv))\n(define inst (wasm-sandbox-instantiate mod-h))\n(wasm-sandbox-call inst \"factorial\" 12) ;; => 479001600\n(wasm-sandbox-free inst)\n(wasm-sandbox-free-module mod-h)\n```\n\n---\n\n## Example 2: Memory-Based Data Processing\n\nWASM linear memory is a flat byte array. Use it for arrays, buffers, and\nstructured data.\n\n```scheme\n(define bv (compile-program\n  '((define-memory 1)  ;; 1 page = 64 KiB\n\n    ;; Sum an array of i32 values at [base, base + count*4)\n    (define (sum-array (base i32) (count i32) -> i32)\n      (let ((total 0) (i 0))\n        (while (< i count)\n          (set! total (+ total (i32.load (+ base (* i 4)))))\n          (set! i (+ i 1)))\n        total))\n\n    ;; Find maximum in an i32 array\n    (define (max-array (base i32) (count i32) -> i32)\n      (let ((best (i32.load base)) (i 1))\n        (while (< i count)\n          (let ((val (i32.load (+ base (* i 4)))))\n            (when (> val best)\n              (set! best val)))\n          (set! i (+ i 1)))\n        best))\n\n    ;; In-place bubble sort on an i32 array\n    (define (sort-array (base i32) (count i32) -> i32)\n      (let ((i 0))\n        (while (< i (- count 1))\n          (let ((j 0))\n            (while (< j (- count i 1))\n              (let ((addr-j (+ base (* j 4)))\n                    (addr-j1 (+ base (* (+ j 1) 4))))\n                (let ((a (i32.load addr-j))\n                      (b (i32.load addr-j1)))\n                  (when (> a b)\n                    (i32.store addr-j b)\n                    (i32.store addr-j1 a))))\n              (set! j (+ j 1))))\n          (set! i (+ i 1)))\n        0)))))\n\n;; Use from host\n(define mod-h (wasm-sandbox-load bv))\n(define inst (wasm-sandbox-instantiate mod-h))\n\n;; Write [30, 10, 50, 20, 40] at offset 0 as little-endian i32s\n(wasm-sandbox-memory-write inst 0\n  (u8-list->bytevector\n    '(30 0 0 0  10 0 0 0  50 0 0 0  20 0 0 0  40 0 0 0)))\n\n(wasm-sandbox-call inst \"sum-array\" 0 5)   ;; => 150\n(wasm-sandbox-call inst \"max-array\" 0 5)   ;; => 50\n(wasm-sandbox-call inst \"sort-array\" 0 5)  ;; => 0 (in-place)\n\n;; Read sorted result back\n(wasm-sandbox-memory-read inst 0 20)\n;; => #vu8(10 0 0 0 20 0 0 0 30 0 0 0 40 0 0 0 50 0 0 0)\n\n(wasm-sandbox-free inst)\n(wasm-sandbox-free-module mod-h)\n```\n\n---\n\n## Example 3: A Byte-Level Tokenizer\n\nThis is the core use case: write a lexer in Jerboa's WASM dialect, compile it,\nrun it on untrusted input in the sandbox. The host writes UTF-8 bytes into WASM\nmemory, the WASM tokenizer scans them, and writes token descriptors to a\nseparate memory region that the host reads back.\n\n### Memory layout convention\n\n```\nOffset 0x0000 .. 0x0FFF  (4 KiB)   Input buffer (host writes here)\nOffset 0x1000 .. 0x2FFF  (8 KiB)   Token output: (type:i32, start:i32, end:i32) \u00d7 N\n```\n\n### The tokenizer\n\n```scheme\n(define tokenizer-bytecode\n  (compile-program\n    '((define-memory 1)\n\n      ;; Token output pointer (starts at 0x1000)\n      (define-global out-ptr i32 #t 4096)\n      ;; Token count\n      (define-global token-count i32 #t 0)\n\n      ;; --- Character classes ---\n\n      (define (is-space (c i32) -> i32)\n        (or (= c 32) (= c 9) (= c 10) (= c 13)))\n\n      (define (is-digit (c i32) -> i32)\n        (and (>= c 48) (<= c 57)))\n\n      (define (is-alpha (c i32) -> i32)\n        (or (and (>= c 65) (<= c 90))    ;; A-Z\n            (and (>= c 97) (<= c 122))   ;; a-z\n            (= c 95)))                     ;; _\n\n      (define (is-alnum (c i32) -> i32)\n        (or (is-alpha c) (is-digit c)))\n\n      ;; --- Token emitter ---\n      ;; Token types: 0=EOF, 1=NUMBER, 2=WORD, 3=STRING, 4=OPERATOR, 5=NEWLINE\n\n      (define (emit (typ i32) (start i32) (end i32) -> i32)\n        (let ((p (global.get 0)))\n          (i32.store p typ)\n          (i32.store (+ p 4) start)\n          (i32.store (+ p 8) end)\n          (global.set 0 (+ p 12))\n          (global.set 1 (+ (global.get 1) 1))\n          0))\n\n      ;; --- Main tokenizer ---\n      ;; Input: bytes at offset 0, length passed as argument.\n      ;; Returns: number of tokens emitted.\n\n      (define (tokenize (input-len i32) -> i32)\n        ;; Reset output state\n        (global.set 0 4096)\n        (global.set 1 0)\n        (let ((pos 0))\n          (while (< pos input-len)\n            (let ((ch (i32.load8_u pos)))\n              (if (= ch 10)\n                ;; Newline token\n                (begin (emit 5 pos (+ pos 1))\n                       (set! pos (+ pos 1)))\n                (if (is-space ch)\n                  ;; Skip whitespace\n                  (set! pos (+ pos 1))\n                  (if (is-digit ch)\n                    ;; Number: scan digits\n                    (let ((start pos))\n                      (while (and (< pos input-len)\n                                  (is-digit (i32.load8_u pos)))\n                        (set! pos (+ pos 1)))\n                      (emit 1 start pos))\n                    (if (is-alpha ch)\n                      ;; Word: scan alphanumeric\n                      (let ((start pos))\n                        (while (and (< pos input-len)\n                                    (is-alnum (i32.load8_u pos)))\n                          (set! pos (+ pos 1)))\n                        (emit 2 start pos))\n                      (if (= ch 34)\n                        ;; Double-quoted string: scan to closing quote\n                        (let ((start pos))\n                          (set! pos (+ pos 1))\n                          (while (and (< pos input-len)\n                                      (!= (i32.load8_u pos) 34))\n                            (set! pos (+ pos 1)))\n                          (when (< pos input-len)\n                            (set! pos (+ pos 1)))  ;; skip closing \"\n                          (emit 3 start pos))\n                        ;; Operator: single character\n                        (begin\n                          (emit 4 pos (+ pos 1))\n                          (set! pos (+ pos 1))))))))))\n          ;; Emit EOF\n          (emit 0 pos pos)\n          (global.get 1))))))\n```\n\n### Running the tokenizer\n\n```scheme\n(define mod-h (wasm-sandbox-load tokenizer-bytecode))\n(define inst (wasm-sandbox-instantiate mod-h 'fuel: 5000000))\n\n;; Write input\n(define input (string->utf8 \"count = 42 + x\\n\"))\n(wasm-sandbox-memory-write inst 0 input)\n\n;; Tokenize\n(define n-tokens (wasm-sandbox-call inst \"tokenize\" (bytevector-length input)))\n;; => 6 (WORD, OPERATOR, NUMBER, OPERATOR, WORD, NEWLINE) + EOF = 7\n\n;; Read token table (each token = 12 bytes: type, start, end)\n(define token-data (wasm-sandbox-memory-read inst #x1000 (* n-tokens 12)))\n\n;; Decode tokens back into Scheme\n(define (decode-tokens bv input-bv count)\n  (let loop ((i 0) (tokens '()))\n    (if (= i count) (reverse tokens)\n      (let* ((base (* i 12))\n             (type (bytevector-u32-ref bv base 'little))\n             (start (bytevector-u32-ref bv (+ base 4) 'little))\n             (end (bytevector-u32-ref bv (+ base 8) 'little))\n             (text (utf8->string\n                     (bytevector-copy input-bv start end))))\n        (loop (+ i 1)\n              (cons (list (vector-ref '#(EOF NUMBER WORD STRING OPERATOR NEWLINE)\n                                      type)\n                          text)\n                    tokens))))))\n\n(decode-tokens token-data input n-tokens)\n;; => ((WORD \"count\") (OPERATOR \"=\") (NUMBER \"42\")\n;;     (OPERATOR \"+\") (WORD \"x\") (NEWLINE \"\\n\") (EOF \"\"))\n\n(wasm-sandbox-free inst)\n(wasm-sandbox-free-module mod-h)\n```\n\n---\n\n## Example 4: Parser with Import Callbacks\n\nInstead of writing tokens to memory, the WASM parser calls an imported host\nfunction for each token. This is cleaner for streaming parsers and avoids\noutput-buffer sizing.\n\n```scheme\n;; The WASM program imports an \"emit\" function from the host\n(define parser-bytecode\n  (compile-program\n    '((define-memory 1)\n\n      ;; Import: host-side token handler\n      ;; Args: token_type, start_offset, end_offset\n      (define-import \"env\" \"emit_token\" (i32 i32 i32) ())\n\n      (define (is-space (c i32) -> i32)\n        (or (= c 32) (= c 9)))\n\n      (define (is-digit (c i32) -> i32)\n        (and (>= c 48) (<= c 57)))\n\n      ;; Tokenize: scan digits and words, call emit_token for each\n      (define (parse (len i32) -> i32)\n        (let ((pos 0) (count 0))\n          (while (< pos len)\n            (let ((ch (i32.load8_u pos)))\n              (if (is-space ch)\n                (set! pos (+ pos 1))\n                (if (is-digit ch)\n                  (let ((start pos))\n                    (while (and (< pos len)\n                                (is-digit (i32.load8_u pos)))\n                      (set! pos (+ pos 1)))\n                    (emit_token 1 start pos)\n                    (set! count (+ count 1)))\n                  (let ((start pos))\n                    (set! pos (+ pos 1))\n                    (emit_token 2 start pos)\n                    (set! count (+ count 1)))))))\n          count)))))\n```\n\nTo run this with the **interpreter** (which supports host import functions):\n\n```scheme\n(define rt (make-wasm-runtime))\n\n;; Collected tokens\n(define tokens '())\n\n;; Register import \u2014 the host function called by WASM\n(wasm-runtime-load rt parser-bytecode\n  `((\"env\" . ((\"emit_token\" . ,(lambda (type start end)\n                                  (set! tokens\n                                    (cons (list type start end) tokens))\n                                  0))))))\n\n;; Write input and run\n(define input (string->utf8 \"42 + 7\"))\n(wasm-runtime-memory-set! rt 0 input)\n(wasm-runtime-call rt \"parse\" (bytevector-length input))\n\n(reverse tokens)\n;; => ((1 0 2) (2 3 4) (1 5 6))\n;; Token type 1 = number at \"42\", type 2 = operator at \"+\", type 1 = number at \"7\"\n```\n\n**Note**: Import callbacks currently work with the Scheme interpreter runtime.\nThe Rust wasmi sandbox does not yet expose host function registration (the WASM\nmodule must be self-contained). This is a planned enhancement.\n\n---\n\n## Example 5: State Machine Protocol Parser\n\nParse a simple key=value protocol (like HTTP headers or .env files) entirely\nin WASM.\n\n```scheme\n(define kv-parser-bytecode\n  (compile-program\n    '((define-memory 1)\n\n      ;; Memory layout:\n      ;;   0x0000 .. 0x1FFF  Input (8 KiB)\n      ;;   0x2000 .. 0x3FFF  Output: pairs as (key_start, key_end, val_start, val_end) \u00d7 N\n      (define-global out-ptr i32 #t 8192)\n      (define-global pair-count i32 #t 0)\n\n      (define (emit-pair (ks i32) (ke i32) (vs i32) (ve i32) -> i32)\n        (let ((p (global.get 0)))\n          (i32.store p ks)\n          (i32.store (+ p 4) ke)\n          (i32.store (+ p 8) vs)\n          (i32.store (+ p 12) ve)\n          (global.set 0 (+ p 16))\n          (global.set 1 (+ (global.get 1) 1))\n          0))\n\n      ;; States: 0=start-of-line, 1=in-key, 2=after-eq, 3=in-value\n      (define (parse-kv (len i32) -> i32)\n        (global.set 0 8192)\n        (global.set 1 0)\n        (let ((pos 0) (state 0) (mark 0) (key-start 0) (key-end 0))\n          (while (< pos len)\n            (let ((ch (i32.load8_u pos)))\n              (if (= state 0)\n                ;; Start of line: skip whitespace, begin key\n                (if (or (= ch 10) (= ch 13) (= ch 32))\n                  (set! pos (+ pos 1))\n                  (if (= ch 35)\n                    ;; Comment: skip to end of line\n                    (begin\n                      (while (and (< pos len) (!= (i32.load8_u pos) 10))\n                        (set! pos (+ pos 1)))\n                      (set! pos (+ pos 1)))\n                    (begin\n                      (set! key-start pos)\n                      (set! state 1)\n                      (set! pos (+ pos 1)))))\n                (if (= state 1)\n                  ;; In key: scan to '='\n                  (if (= ch 61)\n                    (begin\n                      (set! key-end pos)\n                      (set! state 2)\n                      (set! pos (+ pos 1)))\n                    (set! pos (+ pos 1)))\n                  (if (= state 2)\n                    ;; After '=': start value\n                    (begin\n                      (set! mark pos)\n                      (set! state 3)\n                      (set! pos (+ pos 1)))\n                    ;; In value: scan to newline\n                    (if (= ch 10)\n                      (begin\n                        (emit-pair key-start key-end mark pos)\n                        (set! state 0)\n                        (set! pos (+ pos 1)))\n                      (set! pos (+ pos 1))))))))\n          ;; Handle final value without trailing newline\n          (when (= state 3)\n            (emit-pair key-start key-end mark pos))\n          (global.get 1))))))\n```\n\nUsage:\n\n```scheme\n(define mod-h (wasm-sandbox-load kv-parser-bytecode))\n(define inst (wasm-sandbox-instantiate mod-h))\n\n(define input (string->utf8\n  \"HOST=localhost\\nPORT=8080\\n# comment\\nDEBUG=true\\n\"))\n(wasm-sandbox-memory-write inst 0 input)\n\n(define n-pairs (wasm-sandbox-call inst \"parse-kv\" (bytevector-length input)))\n;; => 3\n\n;; Read key-value pairs (16 bytes each)\n(define pair-data (wasm-sandbox-memory-read inst #x2000 (* n-pairs 16)))\n\n(define (decode-kv-pairs bv input-bv count)\n  (let loop ((i 0) (pairs '()))\n    (if (= i count) (reverse pairs)\n      (let* ((base (* i 16))\n             (ks (bytevector-u32-ref bv base 'little))\n             (ke (bytevector-u32-ref bv (+ base 4) 'little))\n             (vs (bytevector-u32-ref bv (+ base 8) 'little))\n             (ve (bytevector-u32-ref bv (+ base 12) 'little)))\n        (loop (+ i 1)\n              (cons (cons (utf8->string (bytevector-copy input-bv ks ke))\n                          (utf8->string (bytevector-copy input-bv vs ve)))\n                    pairs))))))\n\n(decode-kv-pairs pair-data input n-pairs)\n;; => ((\"HOST\" . \"localhost\") (\"PORT\" . \"8080\") (\"DEBUG\" . \"true\"))\n\n(wasm-sandbox-free inst)\n(wasm-sandbox-free-module mod-h)\n```\n\n---\n\n## Compiling to .wasm Files\n\nSave compiled bytecode to disk for later loading:\n\n```scheme\n;; Build step: compile and save\n(define bv (compile-program '((define (add (a i32) (b i32) -> i32) (+ a b)))))\n(call-with-output-file \"add.wasm\"\n  (lambda (p) (put-bytevector p bv))\n  '(binary))\n\n;; Runtime: load from file\n(define bv2 (call-with-input-file \"add.wasm\"\n              (lambda (p) (get-bytevector-all p))\n              '(binary)))\n(define mod-h (wasm-sandbox-load bv2))\n```\n\nThis separates compilation (build time) from execution (runtime). The `.wasm`\nfile is a standard WebAssembly binary \u2014 you can inspect it with `wasm-objdump`\nor `wasm2wat` from the WABT toolkit.\n\n---\n\n## String I/O Conventions\n\nWASM has no string type. Two conventions for passing strings between host and\nWASM:\n\n### Convention A: Memory Buffer (recommended)\n\nHost writes UTF-8 bytes to WASM memory, passes offset and length as arguments.\nWASM writes results to a separate memory region. Host reads results back.\n\n```\nHost                          WASM\n |                              |\n |-- write UTF-8 to [0..N) --->|\n |-- call parse(N) ----------->|\n |                              |-- scan bytes, write tokens to [0x1000..]\n |<-- return token_count -------|\n |-- read tokens from [0x1000] -|\n```\n\nPros: Simple, predictable, no imports needed.\nCons: Output buffer must be pre-sized; host must decode the output format.\n\n### Convention B: Import Callbacks (streaming)\n\nWASM calls an imported host function for each output item. The host function\ncan read the relevant bytes from WASM memory using the offsets provided.\n\n```\nHost                          WASM\n |                              |\n |-- write UTF-8 to [0..N) --->|\n |-- call parse(N) ----------->|\n |                              |-- scan bytes...\n |<-- emit_token(type,s,e) ----|  (host reads bytes [s..e) from WASM memory)\n |<-- emit_token(type,s,e) ----|\n |<-- return token_count -------|\n```\n\nPros: No output buffer sizing, streaming-friendly, host builds native objects.\nCons: Requires import function registration (interpreter mode only, currently).\n\n---\n\n## What Works Well in WASM\n\n| Category | Examples |\n|----------|----------|\n| **Byte-level lexers** | Tokenizers, CSV parsers, line splitters |\n| **State machines** | Protocol parsers, regex DFAs, format validators |\n| **Numeric algorithms** | Crypto primitives, hashing, checksums |\n| **Array processing** | Sorting, searching, filtering on i32/f64 arrays |\n| **Fixed-format parsers** | Binary protocols, TLV, DNS packets |\n| **Recursive descent** | Expression parsers, JSON number/bool parsing |\n| **Pattern matchers** | Glob matching, simple regex engines |\n\n---\n\n## Current Limitations\n\n### No heap allocation\nWASM linear memory is flat. There's no `malloc`/`free`. For tree-building\nparsers, you'd need to implement a bump allocator in WASM:\n\n```scheme\n;; Simple bump allocator pattern\n(define-global heap-ptr i32 #t 16384)  ;; start allocating at 16 KiB\n\n(define (alloc (size i32) -> i32)\n  (let ((ptr (global.get heap-ptr)))\n    (global.set heap-ptr (+ ptr size))\n    ptr))\n```\n\n### No closures or higher-order functions\nFunctions can't capture variables. All data passes through arguments, globals,\nor memory.\n\n### No string operations\nNo `string-append`, `substring`, etc. Strings are raw bytes in memory. The\nhost must handle any string construction.\n\n### No exceptions\nWASM has no try/catch. Use return codes (0 = success, negative = error type).\n\n### Numeric types only\nFunction parameters and return values must be i32, i64, f32, or f64.\nCompound results go through memory.\n\n### No garbage collection\nAllocated memory stays allocated. Use a bump allocator that resets between\ncalls, or implement a free list.\n\n---\n\n## jsh Integration Roadmap\n\nThe goal: write a parser in Jerboa's WASM dialect, compile it to a `.wasm`\nfile, and invoke it from inside jsh as a shell command or pipeline stage.\n\n### Phase 1: `wasm` builtin command\n\nA jsh builtin that loads and calls WASM modules:\n\n```bash\n# Load a .wasm file and call an exported function\nwasm call parser.wasm tokenize \"hello world 42\"\n\n# Use in a pipeline \u2014 stdin flows into WASM memory\necho \"HOST=localhost\" | wasm pipe kv-parser.wasm parse-kv\n\n# Persistent module (load once, call many times)\nwasm load parser parser.wasm\nwasm invoke parser tokenize \"input text\"\nwasm unload parser\n```\n\n### Phase 2: `(jsh wasm)` module\n\nScheme-level API for jsh scripts and meta-commands:\n\n```scheme\n;; In jsh meta-command mode (,expr)\n,use \"parser.wasm\"\n,(wasm-call \"parser.wasm\" 'tokenize \"count = 42\")\n```\n\n### Phase 3: Pipeline integration\n\nWASM modules as pipeline stages with stdin/stdout:\n\n```bash\n# WASM tokenizer as a filter\ncat source.txt | wasm pipe tokenizer.wasm tokenize | grep NUMBER\n\n# Chain WASM stages\ncat input.env | wasm pipe kv-parser.wasm parse-kv | wasm pipe validator.wasm check\n```\n\n### Security model\n\nAll jsh WASM execution inherits the full security stack:\n\n- **Fuel metering**: Deterministic termination (no infinite loops)\n- **Memory isolation**: WASM can't read/write jsh process memory\n- **ROP defense**: Rust wasmi keeps WASM execution off the Chez Scheme stack\n- **Import validation**: Capability-gated host function access\n- **Exception boundary**: WASM errors can't leak Chez runtime internals\n- **Module size limits**: Reject oversized `.wasm` files before parsing\n- **Sandbox composition**: jsh's Landlock/seccomp/pledge layers apply on top\n"}
 {"text": "<!-- FILE: docs/completion.md -->\n# Completion: Wiring Fibers into the Clojure Layer\n\n**Goal:** Every Clojure concurrency primitive in Jerboa should be fiber-aware.\nA `go` block should be a fiber, not an OS thread. `core.async` channels\nshould park fibers, not block threads. Atoms, STM, and the component system\nshould all work naturally inside `fiber-httpd` handlers without accidentally\nblocking the scheduler.\n\n**Status:** 2026-04-12 \u2014 All 6 phases implemented.\n\n---\n\n## The Problem\n\nJerboa has two concurrency worlds that don't talk to each other:\n\n| | **Fiber world** | **Clojure world** |\n|---|---|---|\n| Unit of work | Fiber (engine-based green thread) | OS thread (`fork-thread`) |\n| Channel | `fiber-channel` (parks fiber) | `(std csp)` channel (blocks OS thread) |\n| Scheduling | M:N work-stealing on N workers | 1:1 OS threads, unbounded |\n| I/O | epoll-integrated, non-blocking | Blocking syscalls |\n| Atom | N/A (use raw Chez `box`) | `(std misc atom)` \u2014 mutex-locked |\n| STM | N/A | `(std stm)` \u2014 global commit mutex |\n| HTTP | `fiber-httpd` (one fiber/conn) | N/A |\n\n**What breaks today:** If you call `(chan-get! ch)` inside a `fiber-httpd`\nhandler, it blocks the OS worker thread \u2014 starving every fiber on that worker.\nIf you call `(dosync ...)` from two fibers on the same worker, the commit\nmutex can deadlock the scheduler.\n\n---\n\n## Architecture\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  (std clojure)  +  (std csp clj)                \u2502  \u2190 Clojure API surface\n\u2502  get/assoc/atom/swap!/go/>!!/<!!                 \u2502     (unchanged names)\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  (std csp/fiber)  \u2014 fiber-aware CSP channels     \u2502  \u2190 NEW adapter layer\n\u2502  (std atom/fiber)  \u2014 fiber-safe atoms            \u2502\n\u2502  (std stm/fiber)   \u2014 fiber-safe STM             \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  (std fiber)  \u2014 M:N scheduler + work-stealing    \u2502  \u2190 Foundation\n\u2502  (std net io) \u2014 epoll poller                     \u2502\n\u2502  (std net fiber-httpd) \u2014 HTTP server             \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\nThe Clojure API surface stays identical. The backing implementations detect\nwhether they're running inside a fiber context (`current-fiber` returns\nnon-`#f`) and dispatch accordingly \u2014 fiber primitives when in a fiber, OS\nthread primitives when not.\n\n---\n\n## Phase 1: Fiber-Aware core.async\n\n**The single highest-impact change.** `go` becomes `fiber-spawn`, channels\nbecome fiber-channels, and the entire CSP vocabulary works inside\n`fiber-httpd` handlers.\n\n### 1.1 \u2014 `go` spawns a fiber, not a thread\n\n```scheme\n;; Current (std csp clj):\n(define-syntax go\n  (syntax-rules ()\n    [(_ body ...)\n     (let ([ch (make-channel 1)])\n       (fork-thread (lambda () ...))\n       ch)]))\n\n;; New:\n(define-syntax go\n  (syntax-rules ()\n    [(_ body ...)\n     (let ([rt (current-fiber-runtime)])\n       (if rt\n         ;; Inside fiber runtime: spawn a fiber\n         (let ([ch (make-fiber-channel 1)])\n           (fiber-spawn* (lambda ()\n             (guard (exn [#t (fiber-channel-close ch)])\n               (fiber-channel-send ch (begin body ...)))))\n           ch)\n         ;; Outside fiber runtime: fall back to OS thread (existing behavior)\n         (let ([ch (make-channel 1)])\n           (fork-thread (lambda () ...))\n           ch)))]))\n```\n\nWhen running inside `fiber-httpd`, `(go ...)` creates a fiber that costs\n~4KB. When running standalone (tests, scripts), it falls back to OS threads.\nNo user code changes needed.\n\n### 1.2 \u2014 Unified channel type\n\nCreate `(std csp fiber-chan)` that wraps `fiber-channel` with the CSP\nchannel interface:\n\n- `chan-put!` \u2192 `fiber-channel-send` (parks fiber if full)\n- `chan-get!` \u2192 `fiber-channel-recv` (parks fiber if empty)\n- `chan-try-put!` \u2192 `fiber-channel-try-send`\n- `chan-try-get` \u2192 `fiber-channel-try-recv`\n- `chan-close!` \u2192 `fiber-channel-close`\n- Buffer policies: fixed (bounded fiber-channel), sliding, dropping\n\nThe Clojure aliases (`>!`, `<!`, `>!!`, `<!!`) all route through this.\nSince fiber channels already support `fiber-select`, `alts!` maps directly.\n\n### 1.3 \u2014 `alts!` via `fiber-select`\n\n```scheme\n;; Clojure:\n(let [[val ch] (alts! [ch1 ch2 (timeout 1000)])]\n  (println \"got\" val \"from\" ch))\n\n;; Jerboa \u2014 maps to fiber-select:\n(fiber-select\n  [ch1 val => (values val ch1)]\n  [ch2 val => (values val ch2)]\n  [:timeout 1000 => (values nil :timeout)])\n```\n\nProvide an `alts!` function that builds the `fiber-select` call dynamically.\nThis replaces the current `(std csp select)` event-based implementation\nwhen inside a fiber context.\n\n### 1.4 \u2014 `timeout` channels\n\nAlready have `fiber-timeout` which creates a channel that fires after N ms.\nWire it into `(std csp clj)`:\n\n```scheme\n(define (timeout ms)\n  (if (current-fiber-runtime)\n    (fiber-timeout ms)\n    (csp-timeout ms)))  ;; existing OS-thread version\n```\n\n### 1.5 \u2014 `pipeline` / `pipeline-blocking` / `pipeline-async`\n\nThese spawn parallel workers. Inside a fiber runtime, workers should be\nfibers:\n\n- `pipeline`: N fiber workers, all sharing a transducer\n- `pipeline-blocking`: N OS-thread workers via `work-pool-submit!`\n  (for blocking I/O that can't be fibered)\n- `pipeline-async`: Each item gets its own fiber, user provides\n  async-fn that puts result onto a channel\n\n### Deliverable\n\n`(std csp clj)` works identically from user code but runs fibers\ninside `fiber-httpd`. Existing `go`-heavy code gets ~1000x concurrency\nimprovement (OS threads \u2192 fibers) with zero changes.\n\n**Test:** Port the core.async test suite. Run 10K `go` blocks inside\na `fiber-httpd` handler. Verify no OS threads are leaked.\n\n---\n\n## Phase 2: Fiber-Safe Atoms and Watches\n\n### 2.1 \u2014 Non-blocking atom operations\n\nCurrent `(std misc atom)` uses `mutex-acquire` / `mutex-release`. Inside\na fiber, this blocks the OS worker thread. Replace with:\n\n```scheme\n(define (atom-swap! a f . args)\n  (let loop ()\n    (let* ([old (atom-deref a)]\n           [new (apply f old args)])\n      (if (atom-compare-and-set! a old new)\n        (begin (run-watches! a old new) new)\n        (loop)))))\n```\n\nUse CAS (compare-and-set) spin loop instead of mutex. Chez doesn't have\nnative CAS, but we can implement it with a short-held mutex that never\nblocks fibers for more than a few instructions (no I/O or allocation\nunder the lock).\n\nAlternatively, use `(std misc atom)` as-is but document that atom\ncontention is minimal (the mutex is held for nanoseconds, not\nmilliseconds). This may be good enough \u2014 profile before optimizing.\n\n### 2.2 \u2014 Fiber-aware watches\n\nWhen a watch callback does I/O (e.g., logging, sending a notification),\nit must not block the atom's mutex. Current implementation already runs\nwatches outside the lock. Verify this works correctly inside fibers and\nadd a test.\n\n### 2.3 \u2014 Agents backed by fibers\n\nClojure agents use a thread pool for `send` and a separate pool for\n`send-off`. Map these to:\n\n- `send` \u2192 submit to a bounded fiber pool (N fibers, shared channel)\n- `send-off` \u2192 submit to `work-pool-submit!` (for blocking I/O)\n\nAgent error handling (`agent-error`, `restart-agent`, `set-error-handler!`,\n`set-error-mode!`) stays the same.\n\n### Deliverable\n\nAtoms, watches, and agents work safely inside fibers. No mutex\nstarvation, no worker-thread blocking.\n\n---\n\n## Phase 3: Fiber-Safe STM\n\n### 3.1 \u2014 Replace global commit mutex\n\nCurrent `(std stm)` uses a single global mutex for all commits. Two\nfibers in `dosync` on the same worker thread will deadlock (fiber A\nholds the mutex, fiber B on the same worker can't preempt A to release\nit).\n\nOptions:\n\n**Option A \u2014 Optimistic lock-free STM:**\nReplace the commit mutex with a lock-free compare-and-swap on version\nnumbers. Each tvar has a version counter. `dosync` reads versions at\nstart, validates at commit by checking versions haven't changed, and\natomically bumps them. No mutex needed.\n\n```scheme\n(define (tvar-commit! tv expected-version new-val)\n  ;; CAS: if version still matches, update value + version atomically\n  (with-mutex (tvar-lock tv)  ;; per-tvar lock, not global\n    (if (= (tvar-version tv) expected-version)\n      (begin (tvar-version-set! tv (+ expected-version 1))\n             (tvar-value-set! tv new-val)\n             #t)\n      #f)))\n```\n\n**Option B \u2014 Per-tvar locks (MVCC-style):**\nReplace the single global mutex with per-tvar fine-grained locks. Only\nlock the tvars actually written in the transaction. Conflict detection\nvia version stamps.\n\nRecommend Option B \u2014 simpler, proven (this is what Clojure does), and\nper-tvar locks are short-held so fiber starvation is unlikely.\n\n### 3.2 \u2014 `retry` via fiber parking\n\nClojure's `retry` blocks the transaction until a referenced tvar changes.\nMap this to:\n\n```scheme\n(define (stm-retry! read-set)\n  ;; Park the fiber until any tvar in the read-set changes\n  (let ([ch (make-fiber-channel 1)])\n    ;; Register a one-shot watch on each read-set tvar\n    (for-each (lambda (tv)\n      (tvar-add-watch! tv ch)) read-set)\n    ;; Park until any tvar fires\n    (fiber-channel-recv ch)\n    ;; Unregister watches and restart transaction\n    (for-each (lambda (tv)\n      (tvar-remove-watch! tv ch)) read-set)))\n```\n\nThis gives real STM `retry` semantics \u2014 the fiber sleeps until it\nhas a reason to re-run, instead of busy-spinning.\n\n### Deliverable\n\n`(dosync (alter ref f))` works inside fibers without deadlocks. `retry`\nparks the fiber efficiently. Multiple fibers can run concurrent\ntransactions on the same worker thread.\n\n---\n\n## Phase 4: Ring-Style HTTP Middleware\n\n### 4.1 \u2014 Request and response as persistent maps\n\nClojure Ring represents requests and responses as maps. Jerboa's\n`fiber-httpd` uses records. Bridge the gap:\n\n```scheme\n;; Wrap fiber-httpd request record as a Clojure-compatible map\n(define (request->ring req)\n  (hash-map\n    :request-method (request-method req)\n    :uri            (request-path req)\n    :headers        (request-headers req)\n    :body           (request-body req)\n    :server-port    (or (request-header req \"host\") \"\")\n    :scheme         :http))\n\n;; Convert Ring-style response map back to fiber-httpd response\n(define (ring->response m)\n  (respond (get m :status 200)\n           (get m :headers '())\n           (get m :body \"\")))\n```\n\n### 4.2 \u2014 Middleware as function composition\n\n```scheme\n;; Ring middleware: (handler \u2192 handler)\n(define (wrap-logging handler)\n  (lambda (req)\n    (let ([start (current-time 'time-utc)])\n      (let ([resp (handler req)])\n        (log-request req resp start)\n        resp))))\n\n;; Compose middleware (right to left, like Clojure ->):\n(define (ring-app handler . middleware)\n  (fold-left (lambda (h mw) (mw h)) handler middleware))\n\n;; Usage:\n(fiber-httpd-start 8080\n  (ring-app my-handler\n    wrap-logging\n    wrap-json-content-type\n    wrap-cors\n    wrap-exception-handler))\n```\n\n### 4.3 \u2014 Standard middleware library\n\nPort the most-used Ring middleware:\n\n| Middleware | Purpose |\n|---|---|\n| `wrap-json-body` | Parse JSON request body into map |\n| `wrap-json-response` | Serialize response body as JSON |\n| `wrap-params` | Parse query params into `:params` |\n| `wrap-cookies` | Parse/set cookies |\n| `wrap-session` | Session management (in-memory or pluggable) |\n| `wrap-cors` | CORS headers |\n| `wrap-content-type` | Set default content-type |\n| `wrap-not-modified` | 304 responses via ETag/Last-Modified |\n| `wrap-head` | Convert HEAD requests to GET |\n| `wrap-exception` | Catch exceptions, return 500 |\n\n### 4.4 \u2014 Static file serving via sendfile\n\n```scheme\n(define (wrap-static prefix dir)\n  (lambda (handler)\n    (lambda (req)\n      (let ([path (request-path req)])\n        (if (string-prefix? prefix path)\n          (let ([file (path-join dir (substring path (string-length prefix)))])\n            (if (file-exists? file)\n              (respond-file file)       ;; uses fiber-sendfile internally\n              (handler req)))\n          (handler req))))))\n```\n\n### Deliverable\n\nA Clojure Ring-compatible middleware stack that runs on `fiber-httpd`.\nClojure web developers can port their middleware chains directly.\n\n---\n\n## Phase 5: Component System Integration\n\n### 5.1 \u2014 Fiber-aware component lifecycle\n\nThe current `(std component)` starts/stops components in dependency order.\nAdd fiber-runtime as a first-class component:\n\n```scheme\n(define my-system\n  (system-map\n    :fiber-runtime (fiber-runtime-component 4)\n    :http-server   (httpd-component 8080 handler)\n    :db-pool       (db-pool-component \"postgres://...\")\n    :worker        (worker-component)))\n\n(start my-system)\n;; - Creates fiber runtime (4 workers)\n;; - Starts DB pool\n;; - Starts HTTP server on fiber runtime\n;; - Starts background worker fiber\n```\n\n### 5.2 \u2014 Graceful shutdown integration\n\nWire component `stop` into `fiber-httpd-stop!`:\n\n- Stop accepting new connections\n- Drain in-flight requests (configurable timeout)\n- Close connection pools\n- Stop fiber runtime\n\n### 5.3 \u2014 Dependency injection via fiber parameters\n\nComponents that need access to shared resources (DB pool, config) can use\nfiber parameters:\n\n```scheme\n(define *db-pool* (make-fiber-parameter #f))\n(define *config*  (make-fiber-parameter #f))\n\n;; In httpd handler:\n(lambda (req)\n  (let ([pool (*db-pool*)])\n    (with-pooled-connection pool conn\n      (query conn \"SELECT ...\"))))\n```\n\n### Deliverable\n\nA production-ready application scaffold: fiber runtime + HTTP server +\nDB pool + background workers, all managed by the component system with\nclean startup/shutdown.\n\n---\n\n## Phase 6: Bonus \u2014 core.logic and Datalog\n\nLow priority but high wow-factor. miniKanren was born in Scheme.\n\n### 6.1 \u2014 miniKanren (core.logic subset)\n\n```scheme\n(import (std logic))\n\n(run* (q)\n  (fresh (x y)\n    (== q (list x y))\n    (membero x '(1 2 3))\n    (membero y '(a b))\n    (conde\n      [(== x 1) (== y 'a)]\n      [(== x 2) (== y 'b)])))\n;; => ((1 a) (2 b))\n```\n\nPort `microKanren` (50 lines of Scheme!) then build the `core.logic`\nsugar on top. This is a weekend project.\n\n### 6.2 \u2014 Datalog query engine\n\nBuild on miniKanren + persistent maps for an in-memory Datalog:\n\n```scheme\n(import (std datalog))\n\n(def db (-> (empty-db)\n            (assert [:person/name \"Alice\" :person/age 30])\n            (assert [:person/name \"Bob\"   :person/age 25])))\n\n(query db\n  '[:find ?name ?age\n    :where [?e :person/name ?name]\n           [?e :person/age ?age]\n           [(> ?age 27)]])\n;; => #{[\"Alice\" 30]}\n```\n\n### Deliverable\n\nIn-process logic programming and Datalog queries. Makes Jerboa\ninteresting for rule engines and knowledge graphs.\n\n---\n\n## Priority Order\n\n```\nPhase 1  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588  \u2014 core.async on fibers. Unblocks everything else.\nPhase 2  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588    \u2014 Fiber-safe atoms. Required for real handlers.\nPhase 4  \u2588\u2588\u2588\u2588\u2588\u2588      \u2014 Ring middleware. The thing users actually want.\nPhase 3  \u2588\u2588\u2588\u2588\u2588       \u2014 Fiber-safe STM. Needed for complex state.\nPhase 5  \u2588\u2588\u2588\u2588        \u2014 Component system. Production scaffolding.\nPhase 6  \u2588\u2588\u2588         \u2014 core.logic/Datalog. Differentiator.\n```\n\n## Success Criteria\n\n**We win when:**\n\n```scheme\n(import (jerboa prelude))\n(import (std clojure))\n(import (std csp clj))\n(import (std net fiber-httpd))\n\n(def state (atom {}))\n\n(fiber-httpd-start 8080\n  (lambda (req)\n    (go\n      (let [result (<! (async-fetch-data))]\n        (swap! state assoc :last-result result)\n        (respond-json 200 result)))))\n```\n\nOne import. Fibers, channels, atoms, HTTP \u2014 all wired together.\nNo thread starvation. No deadlocks. 100K concurrent connections.\n"}
 {"text": "<!-- FILE: docs/partial-evaluation.md -->\n# Compile-Time Partial Evaluation\n\n## Overview\n\nThe `(std compiler partial-eval)` library provides compile-time partial evaluation capabilities, allowing the compiler to automatically evaluate what it can at compile time and generate specialized versions of functions based on known static arguments.\n\n## Key Features\n\n### 1. Binding-Time Analysis\n\nThe system classifies expressions as either static (compile-time known) or dynamic (runtime dependent):\n\n```scheme\n(import (std compiler partial-eval))\n\n;; Static values - known at compile time\n(static-value? 42)              ; => #t\n(static-value? \"hello\")         ; => #t \n(static-value? #t)              ; => #t\n(static-value? '(quote data))   ; => #t\n\n;; Dynamic values - require runtime evaluation\n(dynamic-value? 'variable)      ; => #t\n(dynamic-value? '(input-port))  ; => #t\n```\n\n### 2. Partial Evaluation Engine\n\nThe core partial evaluator can evaluate expressions when some arguments are static:\n\n```scheme\n;; Create static environment\n(define static-env (make-hashtable symbol-hash eq?))\n(hashtable-set! static-env 'width 800)\n(hashtable-set! static-env 'height 600)\n\n;; Partially evaluate expressions\n(partial-evaluate '(+ width height) static-env)\n;; => 1400\n\n(partial-evaluate '(+ width x) static-env)  \n;; => (+ 800 x)  ; width is folded, x remains dynamic\n\n(partial-evaluate '(* (+ width height) scale) static-env)\n;; => (* 1400 scale)  ; inner addition is folded\n```\n\n### 3. Function Specialization\n\n#### Manual Specialization\n\nUse `define-specialized` to create specialized versions of existing functions:\n\n```scheme\n;; Original function\n(define (rectangle-area width height) (* width height))\n\n;; Specialized for squares (width = height)\n(define-specialized square-area (rectangle-area x) x)\n\n(square-area 10)  ; => 100, equivalent to (rectangle-area 10 10)\n\n;; Specialized for standard aspect ratio\n(define-specialized hd-area (rectangle-area 1920) height)\n\n(hd-area 1080)  ; => 2073600\n```\n\n#### Automatic Specialization with `define/pe`\n\nMark functions for partial evaluation:\n\n```scheme\n(define/pe (power base exponent)\n  (if (= exponent 0)\n    1\n    (* base (power base (- exponent 1)))))\n\n;; When called with static exponent:\n(power x 3)  ; Can be specialized to (* x (* x (* x 1)))\n             ; Which cp0 optimizes to (* x x x)\n```\n\n### 4. Compile-Time Evaluation\n\nForce evaluation of expressions at compile time:\n\n```scheme\n;; Evaluate at compile time and embed result\n(define screen-pixels \n  (compile-time-eval '(* 1920 1080)))  ; => 2073600\n\n;; Complex compile-time computations\n(define lookup-table\n  (compile-time-eval \n    '(let loop ([i 0] [acc '()])\n       (if (= i 256)\n         (reverse acc)\n         (loop (+ i 1) (cons (* i i) acc))))))\n```\n\n### 5. Built-in Optimized Functions\n\nThe library provides several functions optimized for partial evaluation:\n\n#### Power Function\n```scheme\n(power 2 8)    ; => 256 (specialized at compile time if exponent is static)\n(power x 0)    ; => 1 (always optimized)\n(power x 1)    ; => x (identity optimization)\n```\n\n#### Arithmetic Sequences\n```scheme\n(arithmetic-seq 0 2 5)     ; => (0 2 4 6 8)\n(arithmetic-seq 10 -1 3)   ; => (10 9 8)\n```\n\n#### List Operations\n```scheme\n(define (double x) (* x 2))\n(map-const double '(1 2 3))  ; => (2 4 6)\n```\n\n#### Matrix Operations\n```scheme\n(define matrix '((1 2) (3 4)))\n(matrix-scale matrix 3)  ; => ((3 6) (9 12))\n```\n\n### 6. Function Specialization API\n\n#### Programmatic Specialization\n\n```scheme\n;; Generate specialized function code\n(define spec-code \n  (specialize-function 'multiply '(x y) '(* x y) '(10)))\n\n;; spec-code generates:\n;; (define multiply-specialized (lambda (y) (* 10 y)))\n```\n\n### 7. Configuration and Cache Management\n\n```scheme\n;; Enable/disable auto-specialization\n(enable-auto-specialization!)\n(disable-auto-specialization!)\n(auto-specialization-enabled?)  ; => #t/#f\n\n;; Cache management\n(clear-specialization-cache!)\n(dump-specialization-stats)\n```\n\n## Advanced Usage Patterns\n\n### 1. Domain-Specific Optimization\n\n```scheme\n;; Graphics transformations\n(define/pe (transform-2d x y scale-x scale-y translate-x translate-y)\n  (values (+ (* x scale-x) translate-x)\n          (+ (* y scale-y) translate-y)))\n\n;; When scales and translations are known at compile time,\n;; this generates highly optimized code\n\n;; Specialized for common case: uniform scaling with no translation\n(define-specialized transform-uniform (transform-2d x y scale scale 0 0) x y)\n```\n\n### 2. Configuration-Based Specialization\n\n```scheme\n;; Different algorithms based on compile-time config\n(define/pe (sort-algorithm lst algorithm)\n  (case algorithm\n    [(quick) (quicksort lst)]\n    [(merge) (mergesort lst)]  \n    [(heap)  (heapsort lst)]))\n\n;; Specialized for specific algorithm\n(define-specialized quick-sort (sort-algorithm lst 'quick) lst)\n```\n\n### 3. Loop Unrolling\n\n```scheme\n(define/pe (vector-dot-product a b size)\n  (let loop ([i 0] [sum 0])\n    (if (= i size)\n      sum\n      (loop (+ i 1) (+ sum (* (vector-ref a i) (vector-ref b i)))))))\n\n;; For small static sizes, this unrolls into direct operations\n(vector-dot-product va vb 4)  \n;; => Unrolls to: (+ (* (vector-ref va 0) (vector-ref vb 0))\n;;                   (* (vector-ref va 1) (vector-ref vb 1))  \n;;                   (* (vector-ref va 2) (vector-ref vb 2))\n;;                   (* (vector-ref va 3) (vector-ref vb 3)))\n```\n\n## Performance Considerations\n\n### Benefits\n- **Compile-time computation**: Static values computed once at compile time\n- **Reduced branches**: Static conditionals eliminated\n- **Specialized code paths**: Functions optimized for specific argument patterns\n- **Loop unrolling**: Small loops converted to straight-line code\n\n### Costs\n- **Compilation time**: Partial evaluation adds to compile-time overhead\n- **Code size**: Specialization can increase binary size\n- **Analysis overhead**: Binding-time analysis has computational cost\n\n### Best Practices\n\n1. **Use sparingly**: Mark only hot functions with `define/pe`\n2. **Focus on inner loops**: Greatest benefit in tight computational loops  \n3. **Static configuration**: Excellent for compile-time configuration options\n4. **Avoid over-specialization**: Don't specialize functions with many call sites\n\n## Integration with Chez Scheme's cp0\n\nPartial evaluation works synergistically with Chez Scheme's cp0 optimizer:\n\n1. **PE generates simplified code** \u2192 cp0 performs additional optimizations\n2. **Constant folding** \u2192 cp0 propagates constants further\n3. **Dead code elimination** \u2192 cp0 removes unreachable branches\n4. **Inlining** \u2192 cp0 can inline specialized functions more aggressively\n\n## API Reference\n\n### Core Functions\n- `static-value?` - Test if value is compile-time known\n- `dynamic-value?` - Test if value requires runtime evaluation  \n- `partial-evaluate` - Partially evaluate expression with static environment\n- `compile-time-eval` - Force compile-time evaluation\n\n### Specialization\n- `define/pe` - Mark function for partial evaluation\n- `define-specialized` - Create manually specialized function\n- `specialize-function` - Generate specialized function programmatically\n\n### Configuration\n- `enable-auto-specialization!` - Enable automatic specialization\n- `disable-auto-specialization!` - Disable automatic specialization\n- `auto-specialization-enabled?` - Check specialization status\n\n### Cache Management  \n- `clear-specialization-cache!` - Clear cached specializations\n- `dump-specialization-stats` - Print cache statistics\n\n### Built-in Optimized Functions\n- `power` - Exponentiation with compile-time optimization\n- `arithmetic-seq` - Generate arithmetic sequences\n- `map-const` - Map with constant function\n- `matrix-scale` - Scale matrix by constant\n\nThis implementation provides the foundation for high-performance Scheme code through aggressive compile-time optimization while maintaining the expressiveness and simplicity expected in a Lisp environment."}
-{"text": "<!-- FILE: docs/tutorial.md -->\n# Jerboa Tutorial: Build a URL Shortener\n\nThis tutorial walks you from `hello world` to a deployable single-file\nJerboa service. By the end you will have built **Stubby**, a small URL\nshortener with:\n\n- A `defstruct`-modeled core\n- An in-memory cache and a SQLite persistent store\n- An HTTP API on top of `(std net httpd)`\n- A test suite run by `jerboa test`\n- A packaged, redistributable single-file script\n\nEvery code block in this document is a complete runnable or compilable\nJerboa program \u2014 the project's CI verifies that.\n\n---\n\n## 0. Prerequisites\n\n- Chez Scheme \u2265 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---\n\n## 1. Hello, world\n\nCreate `stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Stubby is starting up.\")\n```\n\nRun it:\n\n```bash\njerboa run stubby.ss\n```\n\nOne import, one line of code. `(jerboa prelude)` is the entire\nlanguage \u2014 no separate core/text/number modules to juggle. Everything\nyou'll use below (structs, hash tables, regex, JSON, format, match)\nlives in the prelude.\n\n---\n\n## 2. Model the domain with `defstruct`\n\nA short link maps a **slug** (`abc123`) to a **target URL** and tracks\nhow many times it has been resolved.\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target)\n  (make-short-link slug target 0))\n\n(def (bump-hits! link)\n  (short-link-hits-set! link (+ (short-link-hits link) 1)))\n\n(let ([ln (new-link \"abc\" \"https://example.com\")])\n  (bump-hits! ln)\n  (bump-hits! ln)\n  (printf \"slug=~a target=~a hits=~a~n\"\n          (short-link-slug ln)\n          (short-link-target ln)\n          (short-link-hits ln)))\n```\n\n`defstruct` autogenerates `make-short-link`, `short-link?`, one\naccessor per field (`short-link-slug`), and mutators with the\n`-set!` suffix (`short-link-hits-set!`). No more boilerplate.\n\n---\n\n## 3. Generate slugs\n\nReal shorteners let users pass a custom slug, but fall back to a\nrandom one. We'll use the prelude's `random` and `string-append`:\n\n```scheme\n(import (jerboa prelude))\n\n(def *alphabet*\n  (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(displayln (random-slug))\n(displayln (random-slug 8))\n```\n\nNotes:\n- The ambiguous characters `0 o l 1` are omitted so slugs stay\n  copy-paste-safe over voice/print.\n- `(random N)` returns an integer in `[0, N)`.\n- `(iota n)` is `0..n-1` \u2014 the Jerboa prelude shadows Chez's\n  stricter version to give you the SRFI-1 interface.\n\n---\n\n## 4. An in-memory store\n\nWrap the map in a tiny module-like closure so the rest of the program\nnever touches the hash table directly:\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target) (make-short-link slug target 0))\n\n(def (make-store)\n  (let ([ht (make-hash-table)])\n    (lambda (op . args)\n      (case op\n        [(put!)   (hash-put! ht (car args) (cadr args))]\n        [(get)    (hash-get ht (car args))]\n        [(has?)   (hash-key? ht (car args))]\n        [(all)    (hash-values ht)]\n        [(remove!) (hash-remove! ht (car args))]\n        [else (error 'store \"unknown op\" op)]))))\n\n(def store (make-store))\n(store 'put! \"abc\" (new-link \"abc\" \"https://example.com\"))\n(store 'put! \"xyz\" (new-link \"xyz\" \"https://jerboa.dev\"))\n\n(printf \"abc \u2192 ~a~n\" (short-link-target (store 'get \"abc\")))\n(printf \"total: ~a links~n\" (length (store 'all)))\n```\n\nThis is a deliberate choice to demonstrate closure-based\nencapsulation; the next section swaps it for SQLite without changing\nthe callers.\n\n---\n\n## 5. Persist to SQLite\n\nAdd `(std db sqlite)` and define a store with the same shape. We keep\na write-through cache so reads stay in-memory but survive restarts:\n\n```scheme\n(import (jerboa prelude)\n        (std db sqlite))\n\n(defstruct short-link (slug target hits))\n\n(def (sqlite-store path)\n  (let ([db (sqlite-open path)]\n        [cache (make-hash-table)])\n    (sqlite-exec db\n      \"CREATE TABLE IF NOT EXISTS links (\n         slug   TEXT PRIMARY KEY,\n         target TEXT NOT NULL,\n         hits   INTEGER NOT NULL DEFAULT 0)\")\n    ;; Warm the cache from disk.\n    (for-each\n      (lambda (row)\n        (hash-put! cache (vector-ref row 0)\n          (make-short-link (vector-ref row 0)\n                           (vector-ref row 1)\n                           (vector-ref row 2))))\n      (sqlite-query db \"SELECT slug, target, hits FROM links\"))\n    (lambda (op . args)\n      (case op\n        [(put!)\n         (let ([ln (cadr args)])\n           (sqlite-eval db\n             \"INSERT OR REPLACE INTO links (slug, target, hits) VALUES (?, ?, ?)\"\n             (short-link-slug ln)\n             (short-link-target ln)\n             (short-link-hits ln))\n           (hash-put! cache (car args) ln))]\n        [(get)    (hash-get cache (car args))]\n        [(has?)   (hash-key? cache (car args))]\n        [(all)    (hash-values cache)]\n        [(bump!)\n         (let ([ln (hash-get cache (car args))])\n           (when ln\n             (short-link-hits-set! ln (+ (short-link-hits ln) 1))\n             (sqlite-eval db\n               \"UPDATE links SET hits = hits + 1 WHERE slug = ?\"\n               (car args))))]\n        [(close!) (sqlite-close db)]\n        [else (error 'store \"unknown op\" op)]))))\n\n;; Smoke test:\n(def db-path \"stubby-demo.db\")\n(when (file-exists? db-path) (delete-file db-path))\n(def st (sqlite-store db-path))\n(st 'put! \"abc\" (make-short-link \"abc\" \"https://example.com\" 0))\n(st 'bump! \"abc\")\n(printf \"abc hits: ~a~n\" (short-link-hits (st 'get \"abc\")))\n(st 'close!)\n(delete-file db-path)\n```\n\nKey patterns:\n- `sqlite-exec` for DDL, `sqlite-eval` for parametrized writes,\n  `sqlite-query` for `SELECT`.\n- Parameters bind via `?` with positional args \u2014 never string-append\n  SQL (injection risk, and `jerboa_security_scan` will flag it).\n- `(case op ...)` dispatches symbol ops cheaply; no method-table\n  overhead is needed at this scale.\n\n---\n\n## 6. Serve it over HTTP\n\nJerboa's `(std net httpd)` mirrors the simple-handler shape you'd\nrecognize from Ring (Clojure) or Rack (Ruby): a handler takes a\nrequest alist and returns a response alist.\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std db sqlite))\n\n(defstruct short-link (slug target hits))\n\n;; --- tiny helpers ---------------------------------------------------\n(def (json-response body (status 200))\n  `((status . ,status)\n    (headers . ((\"Content-Type\" . \"application/json\")))\n    (body . ,(json-object->string body))))\n\n(def (redirect-to url)\n  `((status . 302)\n    (headers . ((\"Location\" . ,url)))\n    (body . \"\")))\n\n(def (not-found msg)\n  (json-response\n    (list->hash-table `((\"error\" . ,msg)))\n    404))\n\n;; --- store (re-uses the sqlite-store closure from \u00a75) --------------\n;; For brevity this example uses an in-memory map; swap in\n;; (sqlite-store \"stubby.db\") for persistence.\n(def store\n  (let ([ht (make-hash-table)])\n    (lambda (op . args)\n      (case op\n        [(put!)  (hash-put! ht (car args) (cadr args))]\n        [(get)   (hash-get ht (car args))]\n        [(has?)  (hash-key? ht (car args))]\n        [(bump!)\n         (let ([ln (hash-get ht (car args))])\n           (when ln\n             (short-link-hits-set! ln (+ (short-link-hits ln) 1))))]))))\n\n;; --- handlers -------------------------------------------------------\n(def (handle-create req)\n  (let* ([query (or (request-query req) \"\")]\n         [target (query-param query \"url\")])\n    (if target\n      (let ([slug (random-slug)])\n        (store 'put! slug (make-short-link slug target 0))\n        (json-response\n          (list->hash-table\n            `((\"slug\" . ,slug)\n              (\"target\" . ,target)))\n          201))\n      (not-found \"missing 'url' query parameter\"))))\n\n(def (handle-follow req)\n  (let ([slug (route-param req \"slug\")])\n    (let ([ln (store 'get slug)])\n      (if ln\n        (begin\n          (store 'bump! slug)\n          (redirect-to (short-link-target ln)))\n        (not-found (format \"no such slug: ~a\" slug))))))\n\n(def (handle-stats req)\n  (let ([slug (route-param req \"slug\")])\n    (let ([ln (store 'get slug)])\n      (if ln\n        (json-response\n          (list->hash-table\n            `((\"slug\"   . ,(short-link-slug ln))\n              (\"target\" . ,(short-link-target ln))\n              (\"hits\"   . ,(short-link-hits ln)))))\n        (not-found \"no such slug\")))))\n\n;; --- glue -----------------------------------------------------------\n(def *alphabet* (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(def (query-param query name)\n  (let loop ([pairs (string-split query \"&\")])\n    (cond\n      [(null? pairs) #f]\n      [else\n       (let ([kv (string-split (car pairs) \"=\")])\n         (if (and (= (length kv) 2) (string=? (car kv) name))\n           (cadr kv)\n           (loop (cdr pairs))))])))\n\n(def routes\n  (make-router\n    (route \"POST\" \"/api/links\"         handle-create)\n    (route \"GET\"  \"/api/links/:slug\"   handle-stats)\n    (route \"GET\"  \"/:slug\"             handle-follow)))\n\n;; Uncomment to serve:\n;; (start-httpd 8080 routes)\n(displayln \"Stubby routes compiled; uncomment start-httpd to serve.\")\n```\n\nTest it with `curl`:\n\n```bash\n# Create a short link:\ncurl -X POST 'http://localhost:8080/api/links?url=https%3A%2F%2Fexample.com'\n# \u2192 {\"slug\":\"k4mq9v\",\"target\":\"https://example.com\"}\n\n# Follow it:\ncurl -v http://localhost:8080/k4mq9v\n# \u2192 302 Location: https://example.com\n\n# Get stats:\ncurl http://localhost:8080/api/links/k4mq9v\n# \u2192 {\"slug\":\"k4mq9v\",\"target\":\"...\",\"hits\":1}\n```\n\n---\n\n## 7. Tests\n\nPut this in `tests/test-stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target) (make-short-link slug target 0))\n\n(def pass 0)\n(def fail 0)\n\n(def (check name actual expected)\n  (if (equal? actual expected)\n    (begin (set! pass (+ pass 1))\n           (printf \"  PASS  ~a~n\" name))\n    (begin (set! fail (+ fail 1))\n           (printf \"  FAIL  ~a~n\" name)\n           (printf \"        expected: ~s~n\" expected)\n           (printf \"        got:      ~s~n\" actual))))\n\n(check \"accessors\"\n  (let ([ln (new-link \"abc\" \"https://example.com\")])\n    (list (short-link-slug ln) (short-link-target ln) (short-link-hits ln)))\n  '(\"abc\" \"https://example.com\" 0))\n\n(check \"hits mutator\"\n  (let ([ln (new-link \"abc\" \"https://example.com\")])\n    (short-link-hits-set! ln 3)\n    (short-link-hits ln))\n  3)\n\n(check \"predicate\"\n  (short-link? (new-link \"a\" \"b\"))\n  #t)\n\n(printf \"~n~a passed, ~a failed~n\" pass fail)\n(when (> fail 0) (exit 1))\n```\n\nRun the suite:\n\n```bash\njerboa test tests/\n```\n\n`jerboa test` discovers every `tests/test-*.ss`, runs each with the\nlibrary path preconfigured, and reports pass/fail counts.\n\nFor heavier projects, use `(std test)` (matchers, fixtures,\ndeterministic output) \u2014 see [testing-and-infrastructure.md](testing-and-infrastructure.md).\n\n---\n\n## 8. Package as a single file\n\nThe moment you want to hand `stubby.ss` to a friend, they shouldn't\nneed to clone the Jerboa repo to run it. The\n[single-file package format](single-file-packages.md) lets you\ndeclare dependencies in the file header and `jerboa exec` auto-installs\nthem:\n\n```scheme\n#!/usr/bin/env -S jerboa exec\n;;; jerboa-package\n;;; name:    stubby\n;;; version: 0.1.0\n;;; requires:\n;;;\n;;; Stubby \u2014 minimalist URL shortener.\n;;; Uses only prelude + std modules, so requires: is empty.\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std db sqlite))\n\n;; ...rest of the program...\n(displayln \"Stubby ready.\")\n```\n\nMake it executable and run:\n\n```bash\nchmod +x stubby.ss\n./stubby.ss\n```\n\nIf Stubby later depends on a third-party Jerboa package (say, a\nMarkdown renderer), you add it to `requires:`:\n\n```\n;;; requires:\n;;;   github.com/alice/jerboa-markdown\n```\n\nOn the recipient's first run, `jerboa exec` prompts once to install\nthe missing dependency and then runs the script. Set\n`JERBOA_EXEC_YES=1` for non-interactive CI.\n\n---\n\n## 9. What to read next\n\n- [anti-cookbook.md](anti-cookbook.md) \u2014 multi-form gotchas that\n  catch newcomers (hash-ref arg order, list-of? as a factory, etc.)\n- [module-quickstarts.md](module-quickstarts.md) \u2014 runnable snippets\n  for 22+ stdlib modules\n- [reader-syntax.md](reader-syntax.md) \u2014 bracket/keyword/heredoc\n  rules with exact desugaring\n- [packages.md](packages.md) \u2014 how to publish Jerboa packages\n- [safety-guide.md](safety-guide.md) \u2014 `(jerboa prelude strict)` and\n  capability-based restrictions\n\n## 10. The finished layout\n\n```\nmy-stubby/\n\u251c\u2500\u2500 stubby.ss           # the single-file service\n\u251c\u2500\u2500 tests/\n\u2502   \u2514\u2500\u2500 test-stubby.ss\n\u2514\u2500\u2500 README.md\n```\n\nThat is a complete, deployable Jerboa project: one source file for\nthe program, one directory of tests, no build system, no manifest.\nIf Stubby grows a second source file, move to the multi-file\nlayout documented in [packages.md](packages.md) \u2014 but resist until\nyou have to.\n"}
+{"text": "<!-- FILE: docs/tutorial.md -->\n# Jerboa Tutorial: Build a URL Shortener\n\nThis tutorial walks you from `hello world` to a deployable single-file\nJerboa service. By the end you will have built **Stubby**, a small URL\nshortener with:\n\n- A `defstruct`-modeled core\n- An in-memory cache and a SQLite persistent store\n- An HTTP API on top of `(std net httpd)`\n- A test suite run by `jerboa test`\n- A packaged, redistributable single-file script\n\nEvery code block in this document is a complete runnable or compilable\nJerboa program \u2014 the project's CI verifies that.\n\n---\n\n## 0. Prerequisites\n\n- Chez Scheme \u2265 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---\n\n## 1. Hello, world\n\nCreate `stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Stubby is starting up.\")\n```\n\nRun it:\n\n```bash\njerboa run stubby.ss\n```\n\nOne import, one line of code. `(jerboa prelude)` is the entire\nlanguage \u2014 no separate core/text/number modules to juggle. Everything\nyou'll use below (structs, hash tables, regex, JSON, format, match)\nlives in the prelude.\n\n---\n\n## 2. Model the domain with `defstruct`\n\nA short link maps a **slug** (`abc123`) to a **target URL** and tracks\nhow many times it has been resolved.\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target)\n  (make-short-link slug target 0))\n\n(def (bump-hits! link)\n  (short-link-hits-set! link (+ (short-link-hits link) 1)))\n\n(let ([ln (new-link \"abc\" \"https://example.com\")])\n  (bump-hits! ln)\n  (bump-hits! ln)\n  (printf \"slug=~a target=~a hits=~a~n\"\n          (short-link-slug ln)\n          (short-link-target ln)\n          (short-link-hits ln)))\n```\n\n`defstruct` autogenerates `make-short-link`, `short-link?`, one\naccessor per field (`short-link-slug`), and mutators with the\n`-set!` suffix (`short-link-hits-set!`). No more boilerplate.\n\n---\n\n## 3. Generate slugs\n\nReal shorteners let users pass a custom slug, but fall back to a\nrandom one. We'll use the prelude's `random` and `string-append`:\n\n```scheme\n(import (jerboa prelude))\n\n(def *alphabet*\n  (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(displayln (random-slug))\n(displayln (random-slug 8))\n```\n\nNotes:\n- The ambiguous characters `0 o l 1` are omitted so slugs stay\n  copy-paste-safe over voice/print.\n- `(random N)` returns an integer in `[0, N)`.\n- `(iota n)` is `0..n-1` \u2014 the Jerboa prelude shadows Chez's\n  stricter version to give you the SRFI-1 interface.\n\n---\n\n## 4. An in-memory store\n\nWrap the map in a tiny module-like closure so the rest of the program\nnever touches the hash table directly:\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target) (make-short-link slug target 0))\n\n(def (make-store)\n  (let ([ht (make-hash-table)])\n    (lambda (op . args)\n      (case op\n        [(put!)   (hash-put! ht (car args) (cadr args))]\n        [(get)    (hash-get ht (car args))]\n        [(has?)   (hash-key? ht (car args))]\n        [(all)    (hash-values ht)]\n        [(remove!) (hash-remove! ht (car args))]\n        [else (error 'store \"unknown op\" op)]))))\n\n(def store (make-store))\n(store 'put! \"abc\" (new-link \"abc\" \"https://example.com\"))\n(store 'put! \"xyz\" (new-link \"xyz\" \"https://jerboa.dev\"))\n\n(printf \"abc \u2192 ~a~n\" (short-link-target (store 'get \"abc\")))\n(printf \"total: ~a links~n\" (length (store 'all)))\n```\n\nThis is a deliberate choice to demonstrate closure-based\nencapsulation; the next section swaps it for SQLite without changing\nthe callers.\n\n---\n\n## 5. Persist to SQLite\n\nAdd `(std db sqlite)` and define a store with the same shape. We keep\na write-through cache so reads stay in-memory but survive restarts:\n\n```scheme\n(import (jerboa prelude)\n        (std db sqlite))\n\n(defstruct short-link (slug target hits))\n\n(def (sqlite-store path)\n  (let ([db (sqlite-open path)]\n        [cache (make-hash-table)])\n    (sqlite-exec db\n      \"CREATE TABLE IF NOT EXISTS links (\n         slug   TEXT PRIMARY KEY,\n         target TEXT NOT NULL,\n         hits   INTEGER NOT NULL DEFAULT 0)\")\n    ;; Warm the cache from disk.\n    (for-each\n      (lambda (row)\n        (hash-put! cache (vector-ref row 0)\n          (make-short-link (vector-ref row 0)\n                           (vector-ref row 1)\n                           (vector-ref row 2))))\n      (sqlite-query db \"SELECT slug, target, hits FROM links\"))\n    (lambda (op . args)\n      (case op\n        [(put!)\n         (let ([ln (cadr args)])\n           (sqlite-eval db\n             \"INSERT OR REPLACE INTO links (slug, target, hits) VALUES (?, ?, ?)\"\n             (short-link-slug ln)\n             (short-link-target ln)\n             (short-link-hits ln))\n           (hash-put! cache (car args) ln))]\n        [(get)    (hash-get cache (car args))]\n        [(has?)   (hash-key? cache (car args))]\n        [(all)    (hash-values cache)]\n        [(bump!)\n         (let ([ln (hash-get cache (car args))])\n           (when ln\n             (short-link-hits-set! ln (+ (short-link-hits ln) 1))\n             (sqlite-eval db\n               \"UPDATE links SET hits = hits + 1 WHERE slug = ?\"\n               (car args))))]\n        [(close!) (sqlite-close db)]\n        [else (error 'store \"unknown op\" op)]))))\n\n;; Smoke test:\n(def db-path \"stubby-demo.db\")\n(when (file-exists? db-path) (delete-file db-path))\n(def st (sqlite-store db-path))\n(st 'put! \"abc\" (make-short-link \"abc\" \"https://example.com\" 0))\n(st 'bump! \"abc\")\n(printf \"abc hits: ~a~n\" (short-link-hits (st 'get \"abc\")))\n(st 'close!)\n(delete-file db-path)\n```\n\nKey patterns:\n- `sqlite-exec` for DDL, `sqlite-eval` for parametrized writes,\n  `sqlite-query` for `SELECT`.\n- Parameters bind via `?` with positional args \u2014 never string-append\n  SQL (injection risk, and `jerboa_security_scan` will flag it).\n- `(case op ...)` dispatches symbol ops cheaply; no method-table\n  overhead is needed at this scale.\n\n---\n\n## 6. Serve it over HTTP\n\nJerboa's `(std net httpd)` mirrors the simple-handler shape you'd\nrecognize from Ring (Clojure) or Rack (Ruby): a handler takes a\nrequest alist and returns a response alist.\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std db sqlite))\n\n(defstruct short-link (slug target hits))\n\n;; --- tiny helpers ---------------------------------------------------\n(def (json-response body (status 200))\n  `((status . ,status)\n    (headers . ((\"Content-Type\" . \"application/json\")))\n    (body . ,(json-object->string body))))\n\n(def (redirect-to url)\n  `((status . 302)\n    (headers . ((\"Location\" . ,url)))\n    (body . \"\")))\n\n(def (not-found msg)\n  (json-response\n    (list->hash-table `((\"error\" . ,msg)))\n    404))\n\n;; --- store (re-uses the sqlite-store closure from \u00a75) --------------\n;; For brevity this example uses an in-memory map; swap in\n;; (sqlite-store \"stubby.db\") for persistence.\n(def store\n  (let ([ht (make-hash-table)])\n    (lambda (op . args)\n      (case op\n        [(put!)  (hash-put! ht (car args) (cadr args))]\n        [(get)   (hash-get ht (car args))]\n        [(has?)  (hash-key? ht (car args))]\n        [(bump!)\n         (let ([ln (hash-get ht (car args))])\n           (when ln\n             (short-link-hits-set! ln (+ (short-link-hits ln) 1))))]))))\n\n;; --- handlers -------------------------------------------------------\n(def (handle-create req)\n  (let* ([query (or (request-query req) \"\")]\n         [target (query-param query \"url\")])\n    (if target\n      (let ([slug (random-slug)])\n        (store 'put! slug (make-short-link slug target 0))\n        (json-response\n          (list->hash-table\n            `((\"slug\" . ,slug)\n              (\"target\" . ,target)))\n          201))\n      (not-found \"missing 'url' query parameter\"))))\n\n(def (handle-follow req)\n  (let ([slug (route-param req \"slug\")])\n    (let ([ln (store 'get slug)])\n      (if ln\n        (begin\n          (store 'bump! slug)\n          (redirect-to (short-link-target ln)))\n        (not-found (format \"no such slug: ~a\" slug))))))\n\n(def (handle-stats req)\n  (let ([slug (route-param req \"slug\")])\n    (let ([ln (store 'get slug)])\n      (if ln\n        (json-response\n          (list->hash-table\n            `((\"slug\"   . ,(short-link-slug ln))\n              (\"target\" . ,(short-link-target ln))\n              (\"hits\"   . ,(short-link-hits ln)))))\n        (not-found \"no such slug\")))))\n\n;; --- glue -----------------------------------------------------------\n(def *alphabet* (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(def (query-param query name)\n  (let loop ([pairs (string-split query \"&\")])\n    (cond\n      [(null? pairs) #f]\n      [else\n       (let ([kv (string-split (car pairs) \"=\")])\n         (if (and (= (length kv) 2) (string=? (car kv) name))\n           (cadr kv)\n           (loop (cdr pairs))))])))\n\n(def routes\n  (make-router\n    (route \"POST\" \"/api/links\"         handle-create)\n    (route \"GET\"  \"/api/links/:slug\"   handle-stats)\n    (route \"GET\"  \"/:slug\"             handle-follow)))\n\n;; Uncomment to serve:\n;; (start-httpd 8080 routes)\n(displayln \"Stubby routes compiled; uncomment start-httpd to serve.\")\n```\n\nTest it with `curl`:\n\n```bash\n# Create a short link:\ncurl -X POST 'http://localhost:8080/api/links?url=https%3A%2F%2Fexample.com'\n# \u2192 {\"slug\":\"k4mq9v\",\"target\":\"https://example.com\"}\n\n# Follow it:\ncurl -v http://localhost:8080/k4mq9v\n# \u2192 302 Location: https://example.com\n\n# Get stats:\ncurl http://localhost:8080/api/links/k4mq9v\n# \u2192 {\"slug\":\"k4mq9v\",\"target\":\"...\",\"hits\":1}\n```\n\n---\n\n## 7. Tests\n\nPut this in `tests/test-stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target) (make-short-link slug target 0))\n\n(def pass 0)\n(def fail 0)\n\n(def (check name actual expected)\n  (if (equal? actual expected)\n    (begin (set! pass (+ pass 1))\n           (printf \"  PASS  ~a~n\" name))\n    (begin (set! fail (+ fail 1))\n           (printf \"  FAIL  ~a~n\" name)\n           (printf \"        expected: ~s~n\" expected)\n           (printf \"        got:      ~s~n\" actual))))\n\n(check \"accessors\"\n  (let ([ln (new-link \"abc\" \"https://example.com\")])\n    (list (short-link-slug ln) (short-link-target ln) (short-link-hits ln)))\n  '(\"abc\" \"https://example.com\" 0))\n\n(check \"hits mutator\"\n  (let ([ln (new-link \"abc\" \"https://example.com\")])\n    (short-link-hits-set! ln 3)\n    (short-link-hits ln))\n  3)\n\n(check \"predicate\"\n  (short-link? (new-link \"a\" \"b\"))\n  #t)\n\n(printf \"~n~a passed, ~a failed~n\" pass fail)\n(when (> fail 0) (exit 1))\n```\n\nRun the suite:\n\n```bash\njerboa test tests/\n```\n\n`jerboa test` discovers every `tests/test-*.ss`, runs each with the\nlibrary path preconfigured, and reports pass/fail counts.\n\nFor heavier projects, use `(std test)` (matchers, fixtures,\ndeterministic output) \u2014 see [testing-and-infrastructure.md](testing-and-infrastructure.md).\n\n---\n\n## 8. Package as a single file\n\nThe moment you want to hand `stubby.ss` to a friend, they shouldn't\nneed to clone the Jerboa repo to run it. The\n[single-file package format](single-file-packages.md) lets you\ndeclare dependencies in the file header and `jerboa exec` auto-installs\nthem:\n\n```scheme\n#!/usr/bin/env -S jerboa exec\n;;; jerboa-package\n;;; name:    stubby\n;;; version: 0.1.0\n;;; requires:\n;;;\n;;; Stubby \u2014 minimalist URL shortener.\n;;; Uses only prelude + std modules, so requires: is empty.\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std db sqlite))\n\n;; ...rest of the program...\n(displayln \"Stubby ready.\")\n```\n\nMake it executable and run:\n\n```bash\nchmod +x stubby.ss\n./stubby.ss\n```\n\nIf Stubby later depends on a third-party Jerboa package (say, a\nMarkdown renderer), you add it to `requires:`:\n\n```\n;;; requires:\n;;;   github.com/alice/jerboa-markdown\n```\n\nOn the recipient's first run, `jerboa exec` prompts once to install\nthe missing dependency and then runs the script. Set\n`JERBOA_EXEC_YES=1` for non-interactive CI.\n\n---\n\n## 9. What to read next\n\n- [anti-cookbook.md](anti-cookbook.md) \u2014 multi-form gotchas that\n  catch newcomers (hash-ref arg order, list-of? as a factory, etc.)\n- [module-quickstarts.md](module-quickstarts.md) \u2014 runnable snippets\n  for 22+ stdlib modules\n- [reader-syntax.md](reader-syntax.md) \u2014 bracket/keyword/heredoc\n  rules with exact desugaring\n- [packages.md](packages.md) \u2014 how to publish Jerboa packages\n- [safety-guide.md](safety-guide.md) \u2014 `(jerboa prelude strict)` and\n  capability-based restrictions\n\n## 10. The finished layout\n\n```\nmy-stubby/\n\u251c\u2500\u2500 stubby.ss           # the single-file service\n\u251c\u2500\u2500 tests/\n\u2502   \u2514\u2500\u2500 test-stubby.ss\n\u2514\u2500\u2500 README.md\n```\n\nThat is a complete, deployable Jerboa project: one source file for\nthe program, one directory of tests, no build system, no manifest.\nIf Stubby grows a second source file, move to the multi-file\nlayout documented in [packages.md](packages.md) \u2014 but resist until\nyou have to.\n"}
 {"text": "<!-- FILE: docs/index.md -->\n# Jerboa Documentation\n\nUpdated 2026-03-22.\n\n## Project Overview\n\n- [status.md](status.md) \u2014 Current project status, feature inventory, known limitations\n- [whats-new.md](whats-new.md) \u2014 What's new: 33 modules added in latest push\n- [whatsnew.md](whatsnew.md) \u2014 Earlier additions: translator enhancements and initial stdlib\n- [pending.md](pending.md) \u2014 Roadmap: what's left to make Jerboa better\n- [architecture-split.md](architecture-split.md) \u2014 Design: Rust native / chez-* (FFI backends) vs jerboa (application logic)\n- [native-rust.md](native-rust.md) \u2014 Rust native library: architecture, migration status, C ABI design\n- [rocks.md](rocks.md) \u2014 Distribution and packaging\n\n## Security\n\n- [safety-guide.md](safety-guide.md) \u2014 Practical guide: writing secure Jerboa applications\n- [security-reference.md](security-reference.md) \u2014 API reference for all security modules\n- [ai-threat.md](ai-threat.md) \u2014 AI-assisted adversarial threat model\n- [capability.md](capability.md) \u2014 Capability-based security design\n- [import-conflicts.md](import-conflicts.md) \u2014 R6RS import conflict resolution\n\n## Standard Library Reference\n\n- [regex.md](regex.md) \u2014 Unified regex API, composable `rx`/`define-rx` patterns, PEG grammar system, raw string syntax\n- [data-structures.md](data-structures.md) \u2014 Persistent maps, lazy sequences, weak collections, collection protocol, relations, diff, equiv\n- [concurrency-extended.md](concurrency-extended.md) \u2014 Events, custodians, pools, delimited continuations, continuation marks, amb\n- [metaprogramming.md](metaprogramming.md) \u2014 Typeclasses, CK-macros, format compilation, chaperones, advice, binary types\n- [testing-and-infrastructure.md](testing-and-infrastructure.md) \u2014 QuickCheck, assert!, profiling, config, terminal, highlighting, guardian pools, memoization\n- [protocols.md](protocols.md) \u2014 9P2000 filesystem protocol, MessagePack serialization\n- [fiber.md](fiber.md) \u2014 Green threads / fibers with M:N scheduling\n\n## Networking\n\n- `(std net ssh)` \u2014 Full SSH client: connect, exec, shell, SFTP, port forwarding (10 modules, 3,132 lines)\n\n## Language Features\n\n- [typing.md](typing.md) \u2014 Gradual type system: inference, refinements, GADTs, HKTs\n- [pattern-matching.md](pattern-matching.md) \u2014 Pattern matching\n- [effects.md](effects.md) \u2014 Algebraic effects\n- [sequences.md](sequences.md) \u2014 Lazy sequences and iterators\n- [staging.md](staging.md) \u2014 Multi-stage programming and macros\n- [partial-evaluation.md](partial-evaluation.md) \u2014 Partial evaluation\n- [cp0-passes.md](cp0-passes.md) \u2014 Chez cp0 optimization passes\n\n## Concurrency and Distribution\n\n- [concurrency.md](concurrency.md) \u2014 Concurrency safety toolkit\n- [lightweight-concurrency.md](lightweight-concurrency.md) \u2014 Lightweight/green threads\n- [async.md](async.md) \u2014 Async I/O\n- [stm.md](stm.md) \u2014 Software transactional memory\n- [actor-model.md](actor-model.md) \u2014 Actor model\n- [distributed.md](distributed.md) \u2014 Distributed computing\n\n## Build and Deployment\n\n- [build.md](build.md) \u2014 Build system\n- [single-binary.md](single-binary.md) \u2014 Single-binary compilation\n- [static-binary-gotchas.md](static-binary-gotchas.md) \u2014 Static binary pitfalls\n- [compiling-gerbil-projects.md](compiling-gerbil-projects.md) \u2014 Compiling Gerbil projects on Jerboa\n- [packages.md](packages.md) \u2014 Package management\n- [optimization.md](optimization.md) \u2014 Performance optimization\n\n## Rust Native Backend\n\n- [native-rust.md](native-rust.md) \u2014 Rust native backend architecture and implementation\n- [vs-rust.md](vs-rust.md) \u2014 Jerboa vs Rust comparison\n- [ffi.md](ffi.md) \u2014 FFI interface design\n\n## Developer Experience\n\n- [devex.md](devex.md) \u2014 Developer experience features\n- [repl-protocol.md](repl-protocol.md) \u2014 REPL protocol\n\n## Archive\n\nHistorical planning documents are in [archive/](archive/) for reference.\n"}
 {"text": "<!-- FILE: docs/rocks.md -->\n# World-Class REPL & Standard Library Expansion\n\n## 1. SLIME-Inspired REPL (`lib/std/repl.sls`)\n\nThe REPL went from a basic 425-line read-eval-print loop to a **1500-line interactive powerhouse** modeled after Emacs SLIME for Common Lisp.\n\n**Why it rocks:** Common Lisp developers rave about SLIME because it makes the language *feel alive*. Jerboa now has that same energy:\n\n- **Value history** (`*`, `**`, `***`, `$1`, `$2`...) \u2014 Never lose a computed result. Refer back to any previous value by number. This is the #1 thing people miss when moving from CL to other Schemes.\n- **Deep object inspector** \u2014 Drill into any value: hashtables show entries, records show fields, closures show captured names. Essential for understanding complex data at the REPL.\n- **Function tracing** \u2014 Wrap any function to see calls/returns with args. Debug without adding print statements.\n- **Built-in profiling & benchmarking** \u2014 Measure wall time, CPU time, memory allocation, GC pressure. No external tools needed.\n- **Tab completion** \u2014 Complete any symbol in the environment. Makes discovery effortless.\n- **Inline documentation** \u2014 `,doc car` gives you docs instantly. Register your own with `register-doc!`.\n- **Apropos search** \u2014 `,apropos string` finds every symbol containing \"string\". Perfect for exploration.\n- **Data engineering commands** \u2014 `,table` for formatted tables, `,stats` for descriptive statistics, `,freq` for frequency tables, `,json` for JSON output. Turn your REPL into a data workbench.\n- **Balanced paren check** \u2014 Correctly handles strings and comments, so `\")\"` inside a string doesn't confuse it.\n- **Persistent history** \u2014 Saved to `~/.jerboa_history` across sessions.\n\n---\n\n## 2. SWANK-Like TCP Server (`lib/std/repl/server.sls`)\n\nA TCP server that speaks an s-expression protocol, enabling **editor integration**.\n\n**Why it rocks:** This is the bridge between jerboa and editors like Emacs. Any editor that can open a TCP socket can:\n\n- Evaluate code remotely and get structured results\n- Get tab completions\n- Look up documentation\n- Expand macros\n- Navigate the filesystem\n- Query memory usage and thread info\n\nThe protocol uses `(id method args...) \u2192 (id :ok result)` \u2014 dead simple to implement on the editor side. Port discovery via `~/.jerboa-repl-port` means editors auto-connect. Multi-threaded so multiple editor connections work simultaneously.\n\n---\n\n## 3. REPL Middleware (`lib/std/repl/middleware.sls`)\n\nAn extensibility layer for the REPL \u2014 register custom commands, printers, input transformers, and eval hooks.\n\n**Why it rocks:** Every REPL eventually needs customization. Instead of forking the REPL code, middleware lets users:\n\n- Add `,mycommand` commands without touching core REPL code\n- Register custom pretty-printers for their record types\n- Transform input (e.g., `!ls` \u2192 `(system \"ls\")`)\n- Hook into pre/post eval for logging, timing, or side effects\n- Customize the prompt\n\nThis is the pattern that made Express.js and Rack successful \u2014 composable middleware.\n\n---\n\n## 4. Notebook System (`lib/std/repl/notebook.sls`)\n\nJupyter-style literate programming for Scheme. Save REPL sessions as executable `.ss.nb` files with markdown documentation.\n\n**Why it rocks:** Data scientists live in notebooks. This brings that workflow to Scheme:\n\n- Mix code cells with markdown documentation\n- Capture outputs alongside code\n- Export to HTML (shareable reports) or Markdown (documentation)\n- Record live sessions \u2014 start recording, do your work, stop, save\n- Files are valid Scheme \u2014 you can `load` them directly\n\n---\n\n## 5. Shell Execution (`lib/std/os/shell.sls`)\n\nHigh-level shell command execution with multiple output modes.\n\n**Why it rocks:** Every scripting language needs easy shell access. This gives you:\n\n- `(shell \"ls -la\")` \u2014 just get stdout as a string\n- `(shell! \"make build\")` \u2014 raise on failure (fail-fast scripting)\n- `(shell/lines \"ls\")` \u2014 get a list of lines (no manual splitting)\n- `(shell/status cmd)` \u2014 get stdout, stderr, AND exit code separately\n- `(shell-pipe \"ls\" \"grep .ss\" \"wc -l\")` \u2014 Unix pipes as function args\n- `(shell-env cmd alist)` \u2014 run with custom environment\n- `(shell-async cmd)` \u2014 background execution with later collection\n\nReplaces 50 lines of `open-process-ports` boilerplate with one-liners.\n\n---\n\n## 6. Template Engine (`lib/std/text/template.sls`)\n\nMustache-inspired string templates with sections, iteration, and conditionals.\n\n**Why it rocks:** Code generation, email templates, report formatting \u2014 templates are everywhere. This handles:\n\n- `{{name}}` variable substitution\n- `{{#items}}...{{/items}}` iteration over lists\n- `{{#flag}}...{{/flag}}` conditional sections\n- `{{^empty}}fallback{{/empty}}` inverted sections\n- Compile once, render many times (fast)\n- Works with both symbol and string alist keys\n\nNo external dependencies. No regex. Just clean recursive-descent parsing.\n\n---\n\n## 7. Memoization (`lib/std/misc/memo.sls`)\n\nMemoization with TTL expiry, LRU eviction, and cache introspection.\n\n**Why it rocks:** Memoization is the easiest performance win in functional programming, but most implementations are toy-level. This one is production-grade:\n\n- `(memo fn)` \u2014 simple unbounded memoization\n- `(memo/lru 1000 fn)` \u2014 evicts least-recently-used when cache exceeds size\n- `(memo/ttl 60 fn)` \u2014 entries expire after N seconds (perfect for API caching)\n- `(memo/lru+ttl 1000 60 fn)` \u2014 combined: bounded AND time-limited\n- `(memo-stats fn)` \u2014 hit rate, miss count (is your cache actually helping?)\n- `(defmemo (fib n) ...)` \u2014 syntax sugar for the common case\n\n---\n\n## 8. Retry with Backoff (`lib/std/misc/retry.sls`)\n\nExponential backoff, jitter, predicates, and circuit breaker pattern.\n\n**Why it rocks:** Networks fail. APIs return 503. Databases go down. Without retry logic, your program crashes at 3am. This gives you:\n\n- `(retry thunk 5 1.0)` \u2014 simple retry with fixed delay\n- `(retry/backoff thunk policy)` \u2014 exponential backoff with jitter (prevents thundering herd)\n- `(retry/predicate thunk pred)` \u2014 only retry on specific exceptions\n- **Circuit breaker** \u2014 after N failures, stop trying for a cooldown period. Prevents cascading failures in distributed systems.\n\nThe circuit breaker alone would be a separate library in most ecosystems.\n\n---\n\n## 9. Time Utilities (`lib/std/time.sls`)\n\nHigh-level time operations that Chez doesn't provide out of the box.\n\n**Why it rocks:** Chez has `current-time` but nothing user-friendly. Now you get:\n\n- `(current-timestamp)` \u2014 ISO 8601 string, ready for logs and APIs\n- `(elapsed thunk)` \u2014 measure how long something takes in one call\n- `(time-it \"label\" thunk)` \u2014 print wall + CPU time (like Gerbil's `time`)\n- `(duration->string 3661)` \u2014 \"1h 1m\" (human-readable, auto-scales from \u03bcs to days)\n- **Stopwatch** with lap timing \u2014 perfect for benchmarking multi-phase operations\n- **Throttle/debounce** \u2014 rate-limit function calls (UI patterns, API clients)\n- `(with-timeout 5.0 thunk)` \u2014 kill long-running operations\n\n---\n\n## 10. Result Monad (`lib/std/misc/result.sls`)\n\nRailway-oriented programming without exceptions.\n\n**Why it rocks:** Exceptions are great for unexpected errors but terrible for expected ones (validation, parsing, user input). Result types let you:\n\n- Chain operations that might fail: `(result-> (ok input) (result-map parse) (result-bind validate))`\n- Never forget to handle errors (the type forces you)\n- Collect errors from multiple operations: `(results-collect list-of-results)`\n- Convert between exceptions and results: `(try->result thunk)`\n- Pattern match cleanly: `(result-fold r on-ok on-err)`\n\nThis is the pattern that makes Rust, Haskell, and Elixir code so robust.\n\n---\n\n## 11. Glob Pattern Matching (`lib/std/text/glob.sls`)\n\nFile glob patterns: `*`, `**`, `?`, `[a-z]`, `[!abc]`.\n\n**Why it rocks:** Every time you need to filter files, you reinvent glob matching. Now it's a library:\n\n- `(glob-match? \"*.ss\" \"hello.ss\")` \u2014 pure pattern matching (no filesystem)\n- `(glob-filter \"*.ss\" file-list)` \u2014 filter a list\n- `(glob-expand \"src/**/*.ss\")` \u2014 actual filesystem expansion with recursive `**`\n- `(glob->regex-string pattern)` \u2014 convert to regex for interop\n\nHandles all the edge cases: `**` crosses directories, `*` doesn't, character classes with ranges and negation.\n\n---\n\n## 12. Data Validation (`lib/std/misc/validate.sls`)\n\nComposable validators with structured error messages.\n\n**Why it rocks:** Input validation is tedious and error-prone. Combinators make it declarative:\n\n```scheme\n(define check-user\n  (v-record\n    (list (cons 'name (v-and (v-required \"name\") (v-min-length \"name\" 1)))\n          (cons 'email (v-and (v-required \"email\") (v-pattern \"email\" \"@\")))\n          (cons 'age (v-and (v-integer \"age\") (v-range \"age\" 0 150))))))\n\n(check-user '((name . \"Alice\") (email . \"a@b.com\") (age . 30)))\n; => (values #t '())\n```\n\nValidators compose with `v-and`/`v-or`, work on records/alists, validate collections with `v-each`, and return all errors at once (not just the first one).\n\n---\n\n## 13. Double-Ended Queue (`lib/std/misc/deque.sls`)\n\nO(1) amortized push/pop on both ends using the classic two-list technique.\n\n**Why it rocks:** Lists are great but you can only efficiently access one end. Deques give you both ends, which is essential for:\n\n- BFS algorithms\n- Sliding window problems\n- Work-stealing schedulers\n- Undo/redo stacks\n\nPlus: `deque-map`, `deque-filter`, `list->deque`, bounded mode.\n\n---\n\n## 14. Path Utilities (`lib/std/os/path-util.sls`)\n\nHigher-level filesystem operations that Chez doesn't provide.\n\n**Why it rocks:** Chez has `directory-list` and `file-exists?` but no recursive operations. Now:\n\n- `(path-walk dir proc)` \u2014 Python's `os.walk` for Scheme\n- `(path-find dir predicate)` \u2014 find files matching any condition\n- `(path-glob dir \"*.ss\")` \u2014 find by glob pattern\n- `(with-temp-directory proc)` \u2014 scoped temp dirs with automatic cleanup\n- `(ensure-directory \"a/b/c\")` \u2014 `mkdir -p` equivalent\n- `(copy-file src dst)` \u2014 binary-safe file copy\n\n---\n\n## 15. Text Diff (`lib/std/text/diff.sls`)\n\nLCS-based line diff with unified output and edit distance.\n\n**Why it rocks:** Testing, debugging, and version comparison all need diff:\n\n- `(diff-lines old new)` \u2014 structured diff as `(keep/add/remove line)` entries\n- `(diff-unified \"a\" \"b\" old new)` \u2014 standard unified diff format\n- `(edit-distance \"kitten\" \"sitting\")` \u2014 Levenshtein distance for fuzzy matching\n- `(diff-apply old hunks)` \u2014 apply a diff to reconstruct the new version\n- `(diff-summary hunks)` \u2014 count additions, deletions, unchanged\n\n---\n\n## 16. Ring Buffer (`lib/std/misc/ringbuf.sls`)\n\nFixed-size circular buffer with O(1) operations.\n\n**Why it rocks:** Ring buffers are the backbone of:\n\n- Log rotation (keep last N log entries)\n- Audio/signal processing (sliding windows)\n- Network packet buffers\n- Rate calculation (keep last N timestamps)\n\nWhen full, new elements silently overwrite the oldest. No allocation, no GC pressure, constant memory.\n\n---\n\n## 17. C-Style Printf (`lib/std/text/printf.sls`)\n\n`%d`, `%s`, `%f`, `%x`, `%o`, `%b`, `%e` with width, precision, padding, and alignment.\n\n**Why it rocks:** Chez's `format` is powerful but uses `~a`/`~s` syntax that nobody outside Scheme knows. When porting C/Python/Go code, you want familiar format strings:\n\n```scheme\n(sprintf \"%08x\" 255)           ; => \"000000ff\"\n(sprintf \"%-20s|\" \"hello\")     ; => \"hello               |\"\n(sprintf \"%.2f\" 3.14159)       ; => \"3.14\"\n(sprintf \"%+d\" 42)             ; => \"+42\"\n```\n\nAlso: `cprintf` for stdout, `fprintf*` for ports, `format-one` for single values.\n\n---\n\n## 18. Binary Heap (`lib/std/misc/heap.sls`)\n\nMin-heap and max-heap priority queue with O(log n) operations.\n\n**Why it rocks:** Priority queues are essential for:\n\n- Dijkstra's algorithm\n- Task scheduling (run highest-priority job next)\n- Event-driven simulation\n- Top-K problems\n- Merge K sorted lists\n\n```scheme\n(define h (list->heap < '(5 3 1 4 2)))\n(heap->sorted-list h)  ; => (1 2 3 4 5)\n```\n\nAuto-growing backing array, works with any comparator.\n\n---\n\n## 19. LRU Cache (`lib/std/misc/lru-cache.sls`)\n\nO(1) get/put/evict using hash table + doubly-linked list.\n\n**Why it rocks:** The classic interview question, implemented properly:\n\n- `(lru-cache-get cache key)` \u2014 O(1) lookup\n- `(lru-cache-put! cache key value)` \u2014 O(1) insert with automatic eviction\n- Hit/miss stats with hit rate calculation\n- Key/value iteration in MRU-to-LRU order\n- Thread-safe for read-heavy workloads\n\nMore efficient than `memo/lru` when you need a standalone cache without function wrapping.\n\n---\n\n## 20. Event Emitter (`lib/std/misc/event-emitter.sls`)\n\nNode.js-style pub/sub for decoupled architecture.\n\n**Why it rocks:** When module A needs to notify module B without importing it, events are the answer:\n\n- `(on ee 'data handler)` \u2014 persistent listener\n- `(once ee 'ready handler)` \u2014 fire once then auto-remove\n- `(emit ee 'data 42)` \u2014 fire all handlers\n- `(off ee 'data)` \u2014 unsubscribe\n\nError isolation: one handler crashing doesn't prevent others from running. Essential for plugin architectures and reactive programming.\n\n---\n\n## 21. Trie / Prefix Tree (`lib/std/misc/trie.sls`)\n\nEfficient string prefix operations for autocomplete and search.\n\n**Why it rocks:** The REPL completion engine uses linear search. A trie makes prefix lookup O(k) where k is the prefix length, regardless of dictionary size:\n\n- `(trie-prefix-search t \"str\")` \u2014 all words starting with \"str\"\n- `(trie-autocomplete t \"he\" 10)` \u2014 top 10 completions\n- `(trie-search t \"hello\")` \u2014 exact membership test\n- `(trie-starts-with? t \"hel\")` \u2014 any word with this prefix?\n\nPerfect for command-line autocompletion, spell checking, and IP routing tables.\n\n---\n\n## 22. Token Bucket Rate Limiter (`lib/std/misc/rate-limiter.sls`)\n\nIndustry-standard rate limiting algorithm.\n\n**Why it rocks:** When calling external APIs, you need to respect rate limits or get banned:\n\n- Tokens refill at a constant rate\n- Burst capacity for short spikes\n- `try-acquire` for non-blocking check\n- `acquire!` for blocking wait\n- `with-rate-limit` for clean wrapping\n\nUsed by AWS, Google Cloud, and every major API gateway. Now available in Scheme.\n\n---\n\n## 23. Resource Pool (`lib/std/misc/pool.sls`)\n\nThread-safe generic resource pool with acquire/release semantics.\n\n**Why it rocks:** Database connections, HTTP clients, file handles \u2014 any expensive resource benefits from pooling:\n\n- Creates resources on demand up to max-size\n- Reuses idle resources instead of creating new ones\n- Blocks when pool is exhausted (backpressure)\n- `pool-with-resource` guarantees release via `dynamic-wind`\n- `pool-drain!` for graceful shutdown\n\n---\n\n## 24. Finite State Machine (`lib/std/misc/state-machine.sls`)\n\nDeclarative FSM with transitions, guards, actions, and history.\n\n**Why it rocks:** State machines are the right abstraction for protocols, UI flows, and workflow engines:\n\n```scheme\n(define door (make-state-machine 'locked\n  `((locked   (unlock) unlocked ,log-unlock)\n    (unlocked (open)   opened   ,log-open)\n    (opened   (close)  unlocked ,log-close)\n    (unlocked (lock)   locked   ,log-lock))))\n```\n\n- Declarative transition table (data, not code)\n- Actions fire on transitions\n- Guards can prevent transitions conditionally\n- Full transition history for debugging\n- `sm-can-send?` for UI enable/disable logic\n- On-transition callbacks for cross-cutting concerns\n\n---\n\n## Test Coverage\n\nEvery module has comprehensive tests:\n\n| Batch | Tests | Modules |\n|-------|-------|---------|\n| REPL  | 91    | repl, server, middleware, notebook |\n| Batch 3 | 94  | shell, template, memo, retry, time |\n| Batch 4 | 128 | result, glob, validate, deque, path-util |\n| Batch 5 | 105 | diff, ringbuf, printf, heap, lru-cache |\n| Batch 6 | 70  | event-emitter, trie, rate-limiter, pool, state-machine |\n| **Total** | **488+** | **25 new modules** |\n"}
 {"text": "<!-- FILE: docs/module-quickstarts.md -->\n# Jerboa Module Quickstarts\n\n_One working example per common `(std ...)` module. Each snippet is a\ncomplete runnable `.ss` file \u2014 paste it into `/tmp/x.ss` and run with\n`scheme --libdirs lib --script /tmp/x.ss` from the Jerboa repo root.\nExports referenced here are verified against `api-signatures.json`\n(2026-04-22)._\n\nAll examples assume `(import (jerboa prelude))` at the top \u2014 the\nsnippets add the module-specific import on top of that.\n\n---\n\n## I/O & files\n\n### `(std io)` \u2014 whole-file reads/writes\n\n```scheme\n(import (jerboa prelude) (std io))\n\n(write-all \"/tmp/hello.txt\" \"hi\\nworld\\n\")\n(displayln (read-all \"/tmp/hello.txt\"))\n\n(write-sexp-file \"/tmp/data.sexp\" '((a 1) (b 2)))\n(displayln (read-sexp-file \"/tmp/data.sexp\"))   ; \u2192 ((a 1) (b 2))\n```\n\nExports: `read-all`, `write-all`, `read-sexp-file`, `write-sexp-file`,\n`read-sexp-port`, `write-sexp-port`, `with-input`, `with-output`.\n\n---\n\n### Paths & prelude file helpers\n\n`path-join`, `read-file-string`, `read-file-lines`, `write-file-string`\nare in the **prelude** \u2014 no extra import needed:\n\n```scheme\n(import (jerboa prelude))\n\n(define p (path-join \"/tmp\" \"note.txt\"))\n(write-file-string p \"line 1\\nline 2\\n\")\n(for ((line (in-list (read-file-lines p))))\n  (displayln line))\n```\n\n---\n\n## Text formats\n\n### `(std text json)` \u2014 JSON\n\n```scheme\n(import (jerboa prelude) (std text json))\n\n(define h (string->json-object \"{\\\"name\\\":\\\"Alice\\\",\\\"age\\\":30}\"))\n(hash-ref h \"name\")                        ; \u2192 \"Alice\"\n\n(hash-put! h \"tags\" (vector \"admin\" \"dev\"))\n(displayln (json-object->string h))\n```\n\nJSON objects \u2194 hash tables; JSON arrays \u2194 vectors (not lists).\n\n---\n\n### `(std text yaml)` \u2014 YAML\n\n```scheme\n(import (jerboa prelude) (std text yaml))\n\n;; Parsing: read-yaml-from-string (from full module, not prelude)\n;; See jerboa_module_exports '(std text yaml)' for the full 66-export API \u2014\n;; common entry points: read-yaml-from-string, write-yaml-to-string.\n```\n_YAML module has 66 exports covering streaming, anchors, tags. Use\n`jerboa_module_exports '(std text yaml)'` to see the full list before\nwriting YAML-specific code._\n\n---\n\n### `(std text utf8)` \u2014 byte \u2194 string\n\n```scheme\n(import (jerboa prelude) (std text utf8))\n\n(define bv (string->utf8 \"h\u00e9llo\"))         ; \u2192 bytevector\n(define s  (utf8->string bv))              ; \u2192 \"h\u00e9llo\"\n\n(displayln (bytevector-length bv))         ; 6 (\u00e9 is 2 bytes)\n```\n\n---\n\n### `(std csv)` \u2014 CSV\n\n```scheme\n(import (jerboa prelude) (std csv))\n\n(write-csv-file \"/tmp/a.csv\"\n                '((\"name\" \"age\") (\"Alice\" \"30\") (\"Bob\" \"25\")))\n\n(for ((row (in-list (read-csv-file \"/tmp/a.csv\"))))\n  (displayln row))\n\n;; Row-of-alists mode:\n(define rows (csv->alists \"name,age\\nAlice,30\"))\n(hash-ref (car rows) 'name)                ; \u2192 \"Alice\"\n```\n\n---\n\n### `(std regex)` \u2014 regex (the prelude re-exports the essentials)\n\n```scheme\n(import (jerboa prelude))                   ; re, re-match?, etc. in prelude\n\n(re-match? \"\\\\d+\" \"42\")                    ; \u2192 #t  (full-string match)\n(re-search \"\\\\d+\" \"abc 42 xyz\")            ; \u2192 match-object\n(re-find-all \"\\\\w+\" \"hello world\")         ; \u2192 (\"hello\" \"world\")\n(re-replace-all \"\\\\d\" \"abc1def2\" \"X\")      ; \u2192 \"abcXdefX\"\n(re-split \"\\\\s+\" \"a   b  c\")               ; \u2192 (\"a\" \"b\" \"c\")\n\n;; SRE (symbolic) patterns:\n(define p (rx (+ digit)))\n(re-match? p \"123\")                        ; \u2192 #t\n```\n\nNamed captures (SRE only):\n```scheme\n(define m (re-search (rx (: (=> year (= 4 digit))\n                            \"-\"\n                            (=> month (= 2 digit))))\n                     \"2026-04\"))\n(re-match-named m 'year)                   ; \u2192 \"2026\"\n```\n\n---\n\n### `(std rx patterns)` \u2014 30+ pre-built named patterns\n\n```scheme\n(import (jerboa prelude) (std rx patterns))\n\n(re-match? rx:email \"user@example.com\")    ; \u2192 #t\n(re-match? rx:uuid \"550e8400-e29b-41d4-a716-446655440000\")\n(re-match? rx:ip4 \"192.168.1.1\")\n```\n\n---\n\n### `(std peg)` \u2014 PEG grammars\n\n```scheme\n(import (jerboa prelude) (std peg))\n\n(define-grammar calc\n  (expr   (/ (: term \"+\" expr) term))\n  (term   (/ digits))\n  (digits (+ (/ \"0\" \"1\" \"2\" \"3\" \"4\" \"5\" \"6\" \"7\" \"8\" \"9\"))))\n;; Then use peg-parse or the grammar's entry point.\n```\n_See `jerboa_module_exports '(std peg)'` \u2014 6 exports around\n`define-grammar` and `peg-error-*`._\n\n---\n\n## Networking\n\n### `(std net request)` \u2014 HTTP client\n\n```scheme\n(import (jerboa prelude) (std net request))\n\n(define r (http-get \"https://httpbin.org/get\"))\n(request-status r)                         ; \u2192 200\n(request-text r)                           ; \u2192 body as string\n\n(define r2 (http-post \"https://httpbin.org/post\"\n                      headers: '((\"Content-Type\" . \"application/json\"))\n                      data: \"{\\\"x\\\":1}\"))\n(request-close r2)                         ; always close\n```\n\nURL helpers: `parse-url`, `url-encode`, `build-query-string`,\n`url-parts-host`, `url-parts-port`, `url-parts-path`, `url-parts-scheme`.\n\n---\n\n### `(std net httpd)` \u2014 HTTP server\n\n```scheme\n(import (jerboa prelude) (std net httpd))\n\n;; http-req-* / http-res-* are the handler-API surface.\n;; See jerboa_howto \"httpd\" for a complete canonical handler template \u2014\n;; don't guess the handler signature; the cookbook has it verified.\n```\n\n_28 exports. Use `jerboa_httpd_handler_scaffold` to generate a stub._\n\n---\n\n## OS\n\n### `(std os env)` \u2014 environment variables\n\n```scheme\n(import (jerboa prelude) (std os env))\n\n(getenv \"PATH\")                            ; \u2192 string or #f\n(setenv \"MY_VAR\" \"hello\")\n(unsetenv \"OLD_VAR\")\n```\n\n---\n\n### `(std os signal)` \u2014 signal constants + handlers\n\n```scheme\n(import (jerboa prelude) (std os signal))\n\nSIGINT SIGTERM SIGKILL                     ; numeric constants\n;; See 32 exports including SIGABRT..SIGXCPU plus handler-install hooks.\n```\n\n---\n\n## Concurrency\n\n### `(std misc thread)` \u2014 threads / mutex / CV\n\n```scheme\n(import (jerboa prelude) (std misc thread))\n\n(define t (spawn (lambda () (displayln \"hi from thread\"))))\n(thread-join! t)\n\n(define m (make-mutex))\n(mutex-lock! m)\n(mutex-unlock! m)\n\n;; thread-sleep! is a Gambit-compat wrapper here\n(thread-sleep! 0.1)\n```\n\nNote: Chez must be built with threading enabled for these to work at\nruntime. If you see `variable make-mutex is not bound`, your Chez build\nis non-threaded.\n\n---\n\n### `(std misc process)` \u2014 subprocess\n\n```scheme\n(import (jerboa prelude) (std misc process))\n\n(define p (open-input-process \"ls /tmp\"))\n(for ((line (in-lines p)))\n  (displayln line))\n(close-input-port p)\n```\n\nExports: `open-process`, `open-input-process`, `open-output-process`,\n`filter-with-process`, + 11 more.\n\n---\n\n### `(std actor)` \u2014 actor system\n\n```scheme\n(import (jerboa prelude) (std actor))\n\n;; 57 exports. Use jerboa_actor_ensemble_scaffold to generate a\n;; canonical actor + supervisor skeleton rather than hand-writing.\n```\n\n---\n\n### `(std async)` \u2014 async/await\n\n```scheme\n(import (jerboa prelude) (std async))\n\n;; Primary primitives: async-channel-get, async-channel-put, and the\n;; Async abstract base. See full 14-export API via jerboa_module_exports.\n```\n\n---\n\n## Data\n\n### `(std sort)` \u2014 sort (Chez arg order: predicate first)\n\n```scheme\n(import (jerboa prelude) (std sort))\n\n(sort < '(3 1 4 1 5 9 2 6))                ; \u2192 (1 1 2 3 4 5 6 9)\n(stable-sort string<? '(\"b\" \"a\" \"c\"))      ; \u2192 (\"a\" \"b\" \"c\")\n\n;; In-place versions mutate the input list:\n(sort! > (list 3 1 2))                     ; \u2192 (3 2 1)\n```\n\n**Do not** write `(sort '(3 1 2) <)` \u2014 that's SRFI-95 / Gerbil order,\nwrong in Jerboa.\n\n---\n\n### `(std datetime)` \u2014 dates & times\n\n```scheme\n(import (jerboa prelude))                   ; datetime basics are in prelude\n\n(define now (datetime-now))\n(datetime->iso8601 now)                    ; \u2192 \"2026-04-22T\u2026\"\n(datetime-year now)                         ; \u2192 2026\n\n(define dt (make-datetime 2026 4 22 12 0 0))\n(datetime<? dt now)                         ; \u2192 #t/#f\n\n(day-of-week dt)                            ; \u2192 symbol\n(leap-year? 2024)                           ; \u2192 #t\n```\n\nFull 50-export API includes `parse-datetime`, `duration`,\n`datetime-add`, `datetime-diff`, floor/truncate ops.\n\n---\n\n### `(std crypto digest)` \u2014 hashes\n\n```scheme\n(import (jerboa prelude) (std crypto digest))\n\n(define h (sha256 (string->utf8 \"hello\")))    ; \u2192 digest object\n(digest->hex-string h)                        ; \u2192 \"2cf24d\u2026\"\n\n;; Available digests: md5, sha1, sha224, sha256, sha384, sha512\n```\n\n---\n\n### `(std crypto hmac)` \u2014 HMAC\n\n```scheme\n(import (jerboa prelude) (std crypto hmac))\n\n(define sig (hmac-sha256 (string->utf8 \"key\")\n                         (string->utf8 \"msg\")))\n(digest->hex-string sig)\n```\n\n---\n\n### `(std db sqlite)` \u2014 SQLite\n\n```scheme\n(import (jerboa prelude) (std db sqlite))\n\n;; 28 exports including SQLITE_OK/SQLITE_DONE constants plus connection\n;; and statement APIs. Use jerboa_howto \"sqlite\" for a verified\n;; connection-pool pattern.\n```\n\n---\n\n## Formatting & logging\n\n### `(std format)` \u2014 `format`, `printf`, `fprintf`\n\n`format` and `printf` are already in the prelude (shadowed versions).\nFor explicit control or to get `eprintf`:\n\n```scheme\n(import (jerboa prelude) (std format))\n\n(printf \"~a / ~a = ~a~%\" 10 3 (/ 10 3))\n(eprintf \"ERROR: ~a~%\" \"oh no\")             ; writes to stderr\n(define s (format \"x=~a\" 42))               ; \u2192 \"x=42\"\n```\n\n---\n\n### `(std logger)` \u2014 structured logging\n\n```scheme\n(import (jerboa prelude) (std logger))\n\n(debugf \"processing ~a items\" 42)\n(current-log-directory \"/var/log/myapp\")    ; parameter\n```\n_12 exports; primary entry points: `debugf`, `current-logger`,\n`current-log-directory`._\n\n---\n\n## Compatibility shims\n\n### `(std gambit-compat)` \u2014 Gambit names\n\n**218 exports.** Import this to get Gambit-style names (e.g.,\n`thread-sleep!`, `time->seconds`, `open-fd-pair`-style aliases) working.\nOnly use when porting Gambit code \u2014 for new code, use the native Jerboa\nspelling from the prelude.\n\n### `(std srfi srfi-1)` \u2014 SRFI-1 list library\n\n```scheme\n(import (jerboa prelude) (std srfi srfi-1))\n\n(fold + 0 '(1 2 3 4))                       ; \u2192 10\n(take '(a b c d e) 2)                       ; \u2192 (a b)  (also in prelude)\n(delete-duplicates '(1 2 1 3 2))            ; \u2192 (1 2 3)\n```\n\n71 exports. Most commonly-used SRFI-1 procedures (`map`, `filter`,\n`fold`, `take`, `drop`) are already in the Jerboa prelude under their\nstandard names.\n\n---\n\n## When in doubt\n\nEvery module's export list is authoritative via:\n\n```\njerboa_module_exports '(std text yaml)'\n```\n\n`api-signatures.json` in `jerboa-mcp/` is the single source of truth for\n\"does symbol X exist and where?\" questions \u2014 parsed directly from .sls\nsource files on every regeneration.\n"}
@@ -371,7 +371,7 @@
 {"text": "<!-- FILE: docs/vs-rust.md -->\n# Jerboa vs Rust for Secure Crypto Infrastructure\n\nAn honest analysis of where GC is a real limitation, where it isn't, and what features Jerboa should adopt from Rust to close the gap.\n\n---\n\n## Table of Contents\n\n1. [The Question](#the-question)\n2. [What GC Actually Prevents](#what-gc-actually-prevents)\n3. [What's Not Fundamental \u2014 Just Missing Features](#whats-not-fundamental--just-missing-features)\n4. [What Jerboa Should Steal from Rust](#what-jerboa-should-steal-from-rust)\n5. [The Right Language for Each Layer](#the-right-language-for-each-layer)\n6. [The Honest Assessment](#the-honest-assessment)\n\n---\n\n## The Question\n\nIf you were building a highly secure crypto infrastructure \u2014 key management, encrypted transport, policy enforcement, audit \u2014 would you choose Rust or Jerboa?\n\nThe naive answer is Rust, because memory safety. The real answer is more nuanced, because \"crypto infrastructure\" is a stack with different requirements at each layer, and GC-based memory safety is a different tradeoff than ownership-based memory safety.\n\n---\n\n## What GC Actually Prevents\n\nThere are exactly three things a garbage collector makes impossible or very hard. These are fundamental to the GC model, not implementation bugs that Chez could fix.\n\n### 1. Guaranteed Secret Wiping\n\nWhen you call `bytevector-fill!` to zero a cryptographic key, the GC may have already copied that bytevector during heap compaction. The old copy sits in the heap until it's overwritten by a future allocation. You can't find it, you can't zero it, and a core dump or cold boot attack recovers it.\n\nRust's `Zeroize` trait has a similar problem with the optimizer (LLVM can eliminate \"dead\" stores to memory that's about to be freed), but `Pin` + `ManuallyDrop` + volatile writes give you a fighting chance. With GC, you have no chance \u2014 the collector moves objects without telling you, and you cannot enumerate all previous locations of an object.\n\n**Impact**: Any time a cryptographic key, password, token, or other secret exists as a Scheme bytevector, copies may persist in the heap indefinitely.\n\n### 2. Deterministic Timing\n\nGC pauses inject unpredictable latency. In a protocol where you need constant-time behavior \u2014 MAC verification must take the same time regardless of where the mismatch is, TLS record processing must not reveal plaintext length through timing \u2014 a GC pause between two timing measurements corrupts the guarantee.\n\nThe primitive operation itself might be constant-time (via OpenSSL's `CRYPTO_memcmp` called through FFI), but the Scheme wrapper around it isn't. If the GC fires between \"start timer\" and \"end timer,\" the attacker sees variable latency and can extract information.\n\n**Impact**: Any Scheme-level timing measurement is unreliable. Constant-time operations must be pushed entirely into C/FFI, with no Scheme allocation between the timing-sensitive start and end points.\n\n### 3. Memory Layout Control\n\nYou can't place two keys in adjacent cache lines to minimize cache side-channel exposure. You can't ensure a nonce counter is in a specific memory region. You can't use `mlock` reliably because GC moves the object after you've locked its page. You can't use `madvise(MADV_DONTDUMP)` on a Scheme object because it might be relocated before the core dump happens.\n\n**Impact**: Side-channel mitigations that depend on memory placement (cache-line isolation, page-level protections) are impossible for GC-managed objects.\n\n### What GC Does NOT Prevent\n\nThese three limitations affect a narrow part of the security stack: **secret material handling and timing-critical operations**. They do not affect:\n\n- Capability enforcement\n- Access control policy logic\n- Audit logging\n- Schema validation\n- Protocol state machines (correctness, not timing)\n- Key management policy (who can use which key for what)\n- Sandbox enforcement\n- Taint tracking\n- Contract verification\n- Effect tracking\n\nFor all of the above, GC is irrelevant to security. The language features that matter are expressiveness, type safety, and correctness guarantees \u2014 areas where Jerboa has advantages over Rust.\n\n---\n\n## What's Not Fundamental \u2014 Just Missing Features\n\nRust has several features that improve security. Most of them are not inherent to Rust's memory model \u2014 they're language features that could be added to Jerboa.\n\n### Compile-Time Exhaustive Matching\n\n**Rust**: `match` on an `enum` is exhaustive \u2014 forget a variant and the compiler rejects it. This matters for security state machines where a missing case means a missing security check.\n\n**Jerboa today**: `match` has a runtime catch-all. Missing a case silently falls through.\n\n**Jerboa could add**:\n\n```scheme\n;; Hypothetical: exhaustive-match that requires listing all known variants\n(defstruct message-type)\n(defstruct (handshake-init message-type) fields: (client-hello))\n(defstruct (handshake-response message-type) fields: (server-hello))\n(defstruct (data message-type) fields: (payload))\n(defstruct (close message-type) fields: (reason))\n\n(exhaustive-match msg message-type\n  [(handshake-init? msg) ...]\n  [(handshake-response? msg) ...]\n  [(data? msg) ...]\n  ;; compile-time error: missing (close? msg) case\n  )\n```\n\n**Implementation**: A `defstruct`-aware macro that knows the variant list for a parent type and verifies at macro-expansion time that all variants are covered. No runtime cost, no language change \u2014 just a macro.\n\n### Compile-Time Lifetime Tracking\n\n**Rust**: The borrow checker catches use-after-free, double-use, and forgotten-drop at compile time.\n\n**Jerboa today**: Linear and affine types catch these at runtime \u2014 use a value twice and you get an error when the code runs, not when it compiles.\n\n**Jerboa could add**: A static analysis pass that checks linear consumption for annotated code. It wouldn't be as complete as Rust's borrow checker (Chez's macro system isn't a full type checker), but it could catch the obvious cases before the code runs:\n\n- Double use of a linear value\n- Scope exit without consuming a linear value\n- Passing a linear value to a function that doesn't consume it\n\nThink of it as a lint pass, not a type system. Catches 80% of the bugs at 20% of the implementation cost.\n\n### Send/Sync \u2014 Compile-Time Thread Safety\n\n**Rust**: `Send` and `Sync` traits prevent data races at compile time. A non-`Send` type cannot be moved to another thread. Period.\n\n**Jerboa today**: Runtime annotations (`defstruct/thread-safe`, `defstruct/immutable`) and STM. Thread safety is enforced by convention and runtime checks.\n\n**Jerboa could add**: A static analysis pass that verifies mutable structs don't cross thread boundaries without synchronization. Inspect `spawn`, `thread-start!`, `channel-put`, and `send` calls for arguments that are mutable, non-thread-safe structs.\n\n### Sum Types with Associated Data\n\n**Rust**: Enums carry data per variant \u2014 `Result<T, E>` is `Ok(T) | Err(E)`. Pattern matching destructures them.\n\n**Jerboa today**: GADTs provide tagged constructors with fields, but they're runtime-checked. `defstruct` with subtypes is close but doesn't integrate with exhaustive matching.\n\n**Jerboa could add**: Tighter integration between `defstruct` variant hierarchies and `match`, enabling the exhaustive matching described above plus ergonomic destructuring.\n\n### No-Panic / Infallible APIs\n\n**Rust**: Distinguishes `Result<T, E>` (recoverable) from `panic!` (unrecoverable). Libraries document which functions can panic and which return `Result`.\n\n**Jerboa today**: Any function can raise any condition. There's no way to declare or verify that a function is total (always returns a value, never raises).\n\n**Jerboa could add**:\n\n```scheme\n;; define/total: statically verified to never raise\n(define/total (parse-config sexp)\n  ;; compile-time error if body contains: error, raise, assert, or calls\n  ;; to non-total functions without guard/catch\n  (validate-schema config-schema sexp))\n```\n\nThis is valuable for security-critical code paths where you need the guarantee that error handling cannot be bypassed by an unexpected exception. If a capability check is `define/total`, you know it always returns allow/deny \u2014 it never crashes in a way that might default to allow.\n\n### Typestate Enforcement\n\n**Rust**: Session types and typestate patterns via `enum` transitions \u2014 a TCP socket in state `Connected` has different methods than one in state `Listening`.\n\n**Jerboa today**: Phantom types provide runtime state tracking with `phantom-check` and `phantom-transition`.\n\n**Jerboa could add**: Compile-time transition checking. The phantom type registry already declares valid transitions \u2014 a macro could verify at expansion time that code only calls methods valid for the declared state.\n\n---\n\n## What Jerboa Should Steal from Rust\n\nRanked by impact on closing the security gap:\n\n### 1. Foreign Region Allocator \u2014 Eliminates the #1 GC Limitation\n\nA non-GC-managed memory region for secrets. This is the single most important feature for crypto infrastructure.\n\n```scheme\n;; Secure memory region outside GC's reach\n(with-secure-region ([key (secure-alloc 32)]\n                     [nonce (secure-alloc 12)])\n  ;; key and nonce are in mlock'd memory, invisible to GC\n  ;; they are bytevector-like but NOT managed by the collector\n  (secure-random-fill! key)\n  (secure-random-fill! nonce)\n  (let ([ciphertext (aead-encrypt key nonce plaintext)])\n    ;; ciphertext is GC-managed (not secret) \u2014 that's fine\n    ciphertext))\n;; key and nonce are zeroed and munlock'd here \u2014 guaranteed\n;; even on exception \u2014 because dynamic-wind + explicit_bzero\n```\n\n**Implementation**:\n\n| Component | Mechanism |\n|-----------|-----------|\n| Allocation | `mmap(MAP_PRIVATE \\| MAP_ANONYMOUS)` via FFI |\n| Page locking | `mlock()` \u2014 prevents swapping to disk |\n| Core dump protection | `madvise(MADV_DONTDUMP)` \u2014 excluded from core dumps |\n| Guard pages | `mprotect(PROT_NONE)` on pages before and after \u2014 catches buffer overflows |\n| Wiping | `explicit_bzero()` \u2014 guaranteed not optimized away |\n| Scope management | `dynamic-wind` \u2014 wipe runs even on exception or continuation escape |\n| GC isolation | Region is outside the Chez heap \u2014 collector never sees it, never copies it |\n\nThis is the pattern libsodium uses (`sodium_malloc` / `sodium_free` / `sodium_mprotect_noaccess`). Chez can implement it entirely via FFI to `mmap`/`mlock`/`madvise`. The secure region is outside the GC heap, so the collector never sees it, never copies it, never leaves remnants.\n\nThe secure allocator would integrate with Jerboa's existing affine types:\n\n```scheme\n;; Secure region values are automatically affine \u2014 use at most once\n;; Guardian ensures wiping even if the programmer forgets\n(define key (secure-alloc/affine 32 cleanup: secure-wipe!))\n;; ... use key ...\n(affine-drop! key)  ;; explicit wipe \u2014 or auto-wipe on scope exit\n```\n\n**Effort**: 3-5 days. The FFI calls are straightforward. The integration with `dynamic-wind` and affine types requires care but Jerboa already has both primitives.\n\n### 2. Compile-Time Exhaustive Match\n\nCatches missed security states at compile time. A TLS state machine with 8 states and a `match` that only handles 7 is a vulnerability \u2014 and today Jerboa only catches it when the 8th state is reached at runtime.\n\n```scheme\n(define-variants tls-state\n  tls-idle tls-handshake-sent tls-handshake-received\n  tls-established tls-closing tls-closed\n  tls-error tls-resumed)\n\n(exhaustive-match (connection-state conn) tls-state\n  [(tls-idle) ...]\n  [(tls-handshake-sent) ...]\n  [(tls-handshake-received) ...]\n  [(tls-established) ...]\n  [(tls-closing) ...]\n  [(tls-closed) ...]\n  [(tls-error) ...]\n  ;; COMPILE-TIME ERROR: missing tls-resumed case\n  )\n```\n\n**Effort**: 2-3 days. Macro that tracks variant registrations and checks coverage at expansion time.\n\n### 3. Static Linearity Checking\n\nCatches use-after-drop before runtime. Today, using a dropped affine value is a runtime error. A lint pass could catch the obvious cases statically.\n\n```scheme\n(with-linear ([key (derive-key password salt)])\n  (encrypt key plaintext)\n  (encrypt key plaintext)  ;; STATIC WARNING: key already consumed\n  )\n```\n\n**Effort**: 3-5 days. Static analysis over SSA-like form of the code. Won't catch everything (higher-order, conditional consumption), but catches the common mistakes.\n\n### 4. Total Function Annotation\n\nGuarantees critical paths complete without exception.\n\n```scheme\n(define/total (check-permission user resource action)\n  ;; Guaranteed to return 'allow or 'deny \u2014 never raises\n  ;; Compiler verifies: no unguarded calls to non-total functions\n  (if (and (valid-user? user)\n           (capability-permits? user resource action))\n    'allow\n    'deny))\n```\n\n**Effort**: 2-3 days. Macro that analyzes body for `error`/`raise` calls and unguarded calls to non-total functions.\n\n### 5. Typestate at Compile Time\n\nExtends phantom types with compile-time transition verification.\n\n```scheme\n(define-protocol tls-protocol\n  (transitions\n    [idle -> handshake-sent]\n    [handshake-sent -> handshake-received error]\n    [handshake-received -> established error]\n    [established -> closing]\n    [closing -> closed]))\n\n;; Compile-time error: cannot transition from idle to established\n(phantom-transition conn 'idle 'established)\n```\n\n**Effort**: 3-5 days. Extends the existing phantom type registry with compile-time checking.\n\n### Summary Table\n\n| Feature | What It Fixes | Effort | Priority |\n|---------|--------------|--------|----------|\n| Secure region allocator | Secret wiping, memory layout, mlock | 3-5 days | Critical |\n| Exhaustive match | Missed security states | 2-3 days | High |\n| Static linearity | Use-after-drop at compile time | 3-5 days | High |\n| Total functions | Exception-bypass in security checks | 2-3 days | Medium |\n| Typestate checking | Protocol state machine correctness | 3-5 days | Medium |\n| Send/Sync analysis | Thread safety | 3-5 days | Lower |\n\n---\n\n## The Right Language for Each Layer\n\nNo single language is optimal for all layers of a crypto infrastructure. The right answer is a stack.\n\n### Crypto Primitives (AES, SHA, ECDSA, key derivation)\n\n**Best language**: C or assembly.\n\nNot because C is safe \u2014 it's the opposite \u2014 but because crypto primitives need:\n\n- Constant-time guarantees (no branch on secret data, no secret-dependent memory access)\n- Precise control over memory layout (to wipe keys, avoid optimizer-eliminated stores)\n- No GC that copies secrets to unpredictable locations\n- SIMD intrinsics for performance (AES-NI, CLMUL, AVX2)\n\nNo high-level language gives you all of that. In practice, you don't write your own \u2014 you use libsodium or OpenSSL's libcrypto.\n\n### Protocol Layer (TLS, key exchange, certificate validation, message framing)\n\n**Best language**: Rust.\n\nThis layer is where most real-world crypto bugs live. Heartbleed was a bounds check bug in the TLS heartbeat handler, not in any cipher. This layer needs:\n\n- Memory safety (buffer overflows are the #1 vulnerability class in protocol implementations)\n- Strong type system (state machines encoded as types)\n- No GC pauses (matters for timing-sensitive protocols)\n- Good FFI to call the C primitives underneath\n\nRust delivers all of this. `rustls` is proof \u2014 a TLS implementation with zero memory safety CVEs, compared to OpenSSL's hundreds.\n\n### Application Security Layer (key management, access control, audit, policy)\n\n**Best language**: Jerboa (with the features above) or a similarly expressive language.\n\nThis layer needs:\n\n| Requirement | Jerboa Feature | Rust Equivalent |\n|-------------|---------------|----------------|\n| Capability-based access control | First-class capabilities with attenuation | Manual, via ownership patterns |\n| Audit logging with mediated I/O | Effect system with handlers | No native support |\n| Sandboxed evaluation | `restricted-eval` with environment control | Not possible without separate process |\n| Runtime contract verification | `define/contract` with pre/post/proves | Only `debug_assert!` |\n| Key policy enforcement | Taint tracking + capabilities + types | External tools (clippy lints) |\n| Hot code reload | Native (load + mtime tracking) | Requires restart |\n| Formal verification hooks | Proof-carrying contracts via macros | Requires external provers |\n| Key lifetime management | Affine types + secure regions + guardians | RAII + `Zeroize` + `Pin` |\n\nThe stack:\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Policy / Key Management / Audit / Access Control               \u2502\n\u2502  Jerboa                                                         \u2502\n\u2502  (capabilities, effects, contracts, taint tracking, sandbox)    \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  Protocol Layer                                                 \u2502\n\u2502  Rust                                                           \u2502\n\u2502  (memory safety, no GC, strong types, state machines)           \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502  Crypto Primitives                                              \u2502\n\u2502  C / assembly (via libsodium or libcrypto)                      \u2502\n\u2502  (constant-time, hardware control, SIMD, timing guarantees)     \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\nEach layer uses the language whose strengths match the threat model at that layer. The primitives need hardware control. The protocol needs memory safety without GC. The policy layer needs expressiveness.\n\n---\n\n## The Honest Assessment\n\n### If Forced to Pick One Language\n\n**Rust**. It's the best single-language compromise: memory safe without GC, good FFI to C primitives, strong enough types for protocol state machines, and while its application layer isn't as expressive as Jerboa's, it's adequate. The ecosystem (ring, rustls, RustCrypto) is mature and audited.\n\n### Where Jerboa Wins Today\n\nFor key management services, policy-driven encryption gateways, or any system where the hard problem is \"who can decrypt what under which conditions\" rather than \"implement AES correctly,\" Jerboa is already more expressive than Rust:\n\n- **Capabilities** can't be forged (after V2 fix), attenuate monotonically, and integrate with the type system\n- **Effects** make I/O explicit and interceptable \u2014 every key operation can be mediated and audited\n- **Contracts** enforce API invariants at every boundary\n- **Taint tracking** prevents secret data from flowing to public outputs\n- **Sandbox** enables safe evaluation of user-provided policy expressions\n- **Hot reload** enables key rotation and policy updates without downtime\n\n### Where Jerboa Loses Today\n\n- **Secret material in memory** \u2014 GC copies keys. Fixed by the secure region allocator.\n- **Timing guarantees** \u2014 GC pauses. Mitigated by doing timing-critical work in FFI.\n- **Compile-time safety** \u2014 exhaustiveness, linearity, thread safety are runtime-only. Fixed by the features above.\n- **Ecosystem maturity** \u2014 Rust has audited, production-deployed crypto libraries. Jerboa has design documents.\n\n### After the Proposed Features\n\nWith the secure region allocator and compile-time exhaustive match, Jerboa's security story becomes:\n\n| Layer | Jerboa Status | vs Rust |\n|-------|--------------|---------|\n| Crypto primitives | Call libsodium/libcrypto via FFI | Equal (both call C) |\n| Secret handling | Secure regions + affine types + guardians | On par with `Zeroize` + `Pin` |\n| Protocol state machines | Exhaustive match + phantom types + typestate | Comparable to Rust enums |\n| Application security | Capabilities + effects + contracts + taint + sandbox | Significantly ahead |\n| Constant-time operations | FFI to C for timing-critical paths | Slight disadvantage (GC pauses around FFI calls) |\n\nGC is the limitation for roughly 5% of the crypto stack \u2014 secret material in memory and timing-critical operations. The secure region allocator covers most of that 5%. The remaining sliver \u2014 true constant-time execution at the Scheme level \u2014 requires C/asm and always will, in any GC'd language.\n\nFor the other 95% of crypto infrastructure, Jerboa is already more expressive than Rust, and the features proposed here would widen that gap.\n"}
 {"text": "<!-- FILE: docs/packages.md -->\n# Package Manager\n\nJerboa includes a semver-based package manager for declaring, resolving,\nand locking dependencies between Jerboa/Chez Scheme libraries.\n\n**Import:** `(jerboa pkg)`\n\n---\n\n## Package Manifest\n\nA package is described by its name, version, and dependencies.\n\n```scheme\n(make-package name version deps description)\n;; name:        symbol\n;; version:     version record (see below)\n;; deps:        list of dep-spec records\n;; description: string\n\n(package? x)\n(package-name pkg)\n(package-version pkg)\n(package-dependencies pkg)\n(package-description pkg)\n```\n\n---\n\n## Versions\n\nVersions follow semantic versioning: `major.minor.patch`.\n\n```scheme\n(make-version major minor patch)\n(version? x)\n(version-major v)\n(version-minor v)\n(version-patch v)\n(version->string v)       ; => \"1.2.3\"\n(string->version \"1.2.3\") ; => #<version 1.2.3>\n\n(version<? v1 v2)         ; #t if v1 < v2\n(version=? v1 v2)         ; #t if v1 = v2\n(version-satisfies? v constraint)\n;; constraint examples:\n;;   \"^1.2.0\"   \u2014 compatible (same major, >= minor.patch)\n;;   \"~1.2.0\"   \u2014 patch compatible (same major.minor, >= patch)\n;;   \">=1.0.0\"  \u2014 minimum version\n;;   \"1.2.3\"    \u2014 exact version\n```\n\n### Constraint semantics\n\n| Constraint | Meaning |\n|------------|---------|\n| `^1.2.3` | `>=1.2.3 <2.0.0` \u2014 compatible with 1.x |\n| `~1.2.3` | `>=1.2.3 <1.3.0` \u2014 patch updates only |\n| `>=1.0.0` | at least 1.0.0 |\n| `1.2.3` | exactly 1.2.3 |\n\n```scheme\n(version-satisfies? (string->version \"1.3.0\") \"^1.2.0\")   ; => #t\n(version-satisfies? (string->version \"2.0.0\") \"^1.2.0\")   ; => #f\n(version-satisfies? (string->version \"1.2.5\") \"~1.2.0\")   ; => #t\n(version-satisfies? (string->version \"1.3.0\") \"~1.2.0\")   ; => #f\n```\n\n---\n\n## Dependency Specs\n\n```scheme\n(make-dep-spec name constraint source)\n;; name:       package name (symbol)\n;; constraint: version constraint string\n;; source:     'registry | \"https://...\" | \"file:../path\"\n\n(dep-spec? x)\n(dep-spec-name ds)\n(dep-spec-constraint ds)\n(dep-spec-source ds)\n```\n\n---\n\n## Package Registry\n\nThe registry maps package names to available versions.\n\n```scheme\n(make-registry)\n(registry? x)\n(registry-add! reg pkg)          ; add a package\n(registry-find reg name version) ; find exact version; returns pkg or #f\n(registry-list reg name)         ; list all versions of a package\n```\n\n```scheme\n(define reg (make-registry))\n\n(registry-add! reg\n  (make-package 'web-server\n    (string->version \"1.0.0\")\n    (list (make-dep-spec 'http-parser \"^2.0.0\" 'registry))\n    \"A simple web server\"))\n\n(registry-add! reg\n  (make-package 'web-server\n    (string->version \"1.1.0\")\n    (list (make-dep-spec 'http-parser \"^2.1.0\" 'registry))\n    \"A simple web server\"))\n\n(registry-list reg 'web-server)\n; => (#<package web-server 1.0.0> #<package web-server 1.1.0>)\n```\n\n---\n\n## Dependency Resolution\n\n```scheme\n(resolve-dependencies registry root-deps)\n;; root-deps: list of dep-specs\n;; Returns: list of package records in topological order (deps before dependents)\n\n(dependency-graph registry deps)\n;; Returns: alist of (name . list-of-dep-names) for the full transitive closure\n\n(topological-sort graph)\n;; graph: alist of (node . (dep-node ...))\n;; Returns: list of nodes in dependency order (leaves first)\n```\n\n```scheme\n(define deps\n  (list (make-dep-spec 'web-server \"^1.0.0\" 'registry)\n        (make-dep-spec 'database   \"~2.3.0\" 'registry)))\n\n(define resolved (resolve-dependencies reg deps))\n;; resolved is a list of packages in build order\n(for-each (lambda (pkg)\n            (printf \"~a ~a~%\"\n              (package-name pkg)\n              (version->string (package-version pkg))))\n          resolved)\n```\n\n---\n\n## Lock File\n\nLock files record the exact resolved versions to ensure reproducible builds.\n\n```scheme\n(make-lock-file packages)   ; packages: list of (name . version-string) pairs\n(lock-file? x)\n\n(lock-file-write lf path)   ; write lock file to path\n(lock-file-read path)       ; read lock file from path; returns lock-file record\n```\n\n### Lock file format (`jerboa.lock`)\n\n```scheme\n;; Generated by jerboa pkg \u2014 do not edit\n((web-server . \"1.0.3\")\n (http-parser . \"2.1.4\")\n (database . \"2.3.1\"))\n```\n\n---\n\n## Package File\n\nThe `jerboa.pkg` file declares a package's metadata.\n\n```scheme\n(read-package-file path)    ; read jerboa.pkg \u2192 package record\n(write-package-file pkg path) ; write package record \u2192 jerboa.pkg\n```\n\n### `jerboa.pkg` format\n\n```scheme\n(package\n  (name my-app)\n  (version \"1.0.0\")\n  (description \"My application\")\n  (dependencies\n    (web-server \"^1.0.0\" registry)\n    (database   \"~2.3.0\" registry)\n    (utils      \">=0.5.0\" \"https://github.com/user/utils\")))\n```\n\n---\n\n## Complete Workflow\n\n```scheme\n(import (chezscheme) (jerboa pkg))\n\n;; 1. Create a registry with available packages\n(define reg (make-registry))\n(for-each (lambda (pkg) (registry-add! reg pkg))\n          (load-packages-from-server \"https://packages.jerboa-lang.org\"))\n\n;; 2. Read this project's package file\n(define this-pkg (read-package-file \"jerboa.pkg\"))\n\n;; 3. Resolve dependencies\n(define resolved\n  (resolve-dependencies reg (package-dependencies this-pkg)))\n\n(printf \"Resolved ~a dependencies:~%\" (length resolved))\n(for-each (lambda (p)\n            (printf \"  ~a ~a~%\"\n              (package-name p)\n              (version->string (package-version p))))\n          resolved)\n\n;; 4. Write lock file\n(lock-file-write\n  (make-lock-file\n    (map (lambda (p)\n           (cons (package-name p)\n                 (version->string (package-version p))))\n         resolved))\n  \"jerboa.lock\")\n\n(printf \"Lock file written.~%\")\n```\n"}
 {"text": "<!-- FILE: docs/cp0-passes.md -->\n# User-Defined cp0 Optimization Passes\n\n## Overview\n\nThe `(std compiler passes)` library exposes Chez Scheme's cp0 optimizer as a programmable interface, allowing users to write custom optimization passes that run during compilation.\n\n## Key Features\n\n### 1. Custom Pass Definition\n```scheme\n(import (std compiler passes))\n\n;; Define a pass using the simplified API\n(define-cp0-pass my-optimization-pass\n  \"Fold mathematical identities\"\n  (lambda (expr)\n    (match expr\n      [(list '+ x 0) x]\n      [(list '* x 1) x]  \n      [(list '* x 0) 0]\n      [_ #f]))  ; Return #f if no transformation applies\n  30)  ; Priority (lower runs first)\n```\n\n### 2. Pattern-Based Transformations\nThe passes use the advanced `match2` pattern matching system:\n\n```scheme\n;; Matrix operation fusion\n(define-cp0-pass matrix-fusion\n  \"Fuse consecutive matrix operations to avoid intermediate allocations\"\n  (lambda (expr)\n    (match expr\n      [(list 'matrix-* (list 'matrix-* a b) c)\n       (list 'matrix-*-fused a b c)]\n      [_ #f])))\n```\n\n### 3. Pass Registration and Management\n```scheme\n;; Register a pass with priority\n(register-optimization-pass! my-pass 25)\n\n;; List all registered passes\n(list-optimization-passes)\n;; => ((pass-name description priority enabled?) ...)\n\n;; Remove a pass\n(unregister-optimization-pass! 'my-pass)\n```\n\n### 4. Pass Composition\n```scheme\n;; Compose multiple passes into a pipeline\n(define my-pipeline\n  (compose-passes \n    pass:constant-fold\n    pass:dead-code-eliminate\n    my-optimization-pass))\n\n;; Apply composed passes\n(my-pipeline '(+ (* 2 3) 0))  ; => 6\n```\n\n### 5. Built-in Optimization Passes\n\nThe library provides several ready-to-use passes:\n\n- **`pass:constant-fold`** - Evaluates constant expressions at compile time\n- **`pass:dead-code-eliminate`** - Removes unreachable code and unused bindings  \n- **`pass:inline-small-functions`** - Inlines functions with small bodies\n- **`pass:loop-unroll`** - Unrolls loops with known small iteration counts\n\n### 6. Debugging Support\n```scheme\n;; Enable pass debugging\n(enable-pass-debug!)\n\n;; Check debug status\n(pass-debug-enabled?)  ; => #t\n\n;; Disable debugging\n(disable-pass-debug!)\n```\n\n## Domain-Specific Optimization Examples\n\n### SQL Query Optimization\n```scheme\n(define-cp0-pass sql-query-fusion\n  \"Combine consecutive SQL operations into single query\"\n  (lambda (expr)\n    (match expr\n      [(list 'sql-filter pred (list 'sql-map fn table))\n       (list 'sql-filter-map pred fn table)]\n      [(list 'sql-sort key (list 'sql-filter pred table))\n       (list 'sql-filter-sort pred key table)]\n      [_ #f])))\n```\n\n### Arithmetic Simplifications  \n```scheme\n(define-cp0-pass arithmetic-simplify\n  \"Simplify arithmetic expressions\"\n  (lambda (expr)\n    (match expr\n      [(list '+ x 0) x]\n      [(list '+ 0 x) x] \n      [(list '* x 1) x]\n      [(list '* 1 x) x]\n      [(list '* x 0) 0]\n      [(list '* 0 x) 0]\n      [(list '- x 0) x]\n      [(list '/ x 1) x]\n      [_ #f])))\n```\n\n## Usage Patterns\n\n### 1. Development Workflow\n```scheme\n;; 1. Define pass\n(define-cp0-pass my-pass \"...\" transformer)\n\n;; 2. Test pass individually  \n(let ([result ((cp0-pass-transformer my-pass) test-expr)])\n  (display result))\n\n;; 3. Register and test in pipeline\n(register-optimization-pass! my-pass)\n(apply-optimization-passes test-expr)\n```\n\n### 2. Performance Considerations\n- Passes run in priority order (lower numbers first)\n- Keep transformers fast - they run on every matching expression\n- Use guards to check expression structure before expensive operations\n- Return `#f` quickly for non-matching patterns\n\n### 3. Best Practices\n\n**Pattern Matching:**\n```scheme\n;; Good: Check structure first\n(lambda (expr)\n  (if (not (pair? expr))\n    #f  ; Fast exit for atoms\n    (match expr\n      [(list 'target-op args ...) (transform args)]\n      [_ #f])))\n```\n\n**Error Handling:**\n```scheme\n;; Good: Handle edge cases\n(lambda (expr)\n  (match expr\n    [(list '/ a b) \n     (if (and (number? a) (number? b) (not (= b 0)))\n       (/ a b)\n       #f)]  ; Don't divide by zero\n    [_ #f]))\n```\n\n## Integration with Chez Scheme\n\nThe passes integrate with Chez Scheme's compilation pipeline by:\n\n1. **AST Transformation** - Operating on s-expression representations\n2. **Type-Preserving** - Maintaining semantic equivalence  \n3. **Composable** - Can be combined with existing cp0 passes\n4. **Debuggable** - Providing introspection and logging\n\n## Performance Impact\n\n- **Compile-time Cost**: Passes add to compilation time proportional to code size\n- **Runtime Benefit**: Optimized code runs faster, reduced allocations\n- **Memory Usage**: Minimal - passes are stateless functions\n- **Scalability**: Linear in number of expressions processed\n\n## API Reference\n\n### Core Functions\n- `define-cp0-pass` - Define an optimization pass\n- `make-cp0-pass` - Create pass record manually\n- `register-optimization-pass!` - Add pass to global registry\n- `unregister-optimization-pass!` - Remove pass from registry\n- `list-optimization-passes` - List all registered passes\n- `apply-optimization-passes` - Apply all passes to expression\n- `compose-passes` - Combine multiple passes\n\n### Debugging\n- `enable-pass-debug!` - Enable debug output\n- `disable-pass-debug!` - Disable debug output  \n- `pass-debug-enabled?` - Check debug status\n- `dump-ir-between-passes!` - Enable IR dumping\n\n### Record Accessors\n- `cp0-pass?` - Test if object is a pass\n- `cp0-pass-name` - Get pass name\n- `cp0-pass-description` - Get pass description\n- `cp0-pass-transformer` - Get transformer function\n- `cp0-pass-priority` - Get/set pass priority\n- `cp0-pass-enabled` - Get/set enabled status\n\nThis implementation provides a powerful foundation for user-defined compiler optimizations while maintaining the safety and composability expected in a Scheme system."}
-{"text": "<!-- FILE: docs/compiling-gerbil-projects.md -->\n# Compiling Gerbil Projects with Jerboa\n\nThis guide walks through the complete process of taking an existing Gerbil\nScheme project and compiling it to run on Chez Scheme via Jerboa.\n\n## Prerequisites\n\n1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://github.com/ober/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```\n\n## Project Layout\n\nA jerboa project wraps an existing Gerbil project with build scripts and a\ncompatibility layer:\n\n```\nmy-jerboa-project/\n  my-gerbil-project/        # Gerbil source (submodule or copy)\n    gerbil.pkg\n    *.ss                     # Gerbil source files (never modified)\n  src/\n    compat/                  # Chez compatibility shims (handwritten)\n      sugar.sls              # :std/sugar equivalents\n      format.sls             # :std/format equivalents\n      ...\n    mylib/                   # Generated .sls files (gitignored)\n  build-jerboa.ss            # Jerboa translation driver\n  build-all.ss               # Chez compilation trigger\n  build-binary.ss            # Optional: standalone binary builder\n  main.ss                    # Entry point program\n  Makefile\n```\n\n## Step 1: Create the Makefile\n\n```makefile\nSCHEME = scheme\nJERBOA = $(or $(JERBOA_DIR),$(HOME)/src/jerboa/src)\nLIBDIRS = src:$(JERBOA)\nCOMPILE = $(SCHEME) -q --libdirs $(LIBDIRS) --compile-imported-libraries\n\n.PHONY: all jerboa compile binary run clean\n\nall: jerboa compile\n\n# Translate .ss -> .sls via jerboa\njerboa:\n\t$(COMPILE) < build-jerboa.ss\n\n# Compile .sls -> .so via Chez\ncompile: jerboa\n\t$(COMPILE) < build-all.ss\n\n# Build standalone binary\nbuild: binary\nbinary: clean jerboa\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program build-binary.ss\n\n# Run interpreted\nrun: all\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program main.ss\n\nclean:\n\tfind src -name '*.so' -o -name '*.wpo' | xargs rm -f 2>/dev/null || true\n\trm -f src/mylib/*.sls\n```\n\nThe `JERBOA` variable points to jerboa's `src/` directory. Override it:\n```bash\nJERBOA_DIR=/path/to/jerboa/src make\n```\n\nIf your project depends on other jerboa-compiled libraries, add them to\n`LIBDIRS`:\n```makefile\nJERBOA_AWS = $(or $(JERBOA_AWS_DIR),jerboa-aws/src)\nLIBDIRS = src:$(JERBOA):$(JERBOA_AWS)\n```\n\n## Step 2: Write the Import Map\n\nThe import map is the most important part. It tells jerboa how to translate\nGerbil's `:module/path` imports into R6RS `(library name)` imports.\n\n```scheme\n(define my-import-map\n  '(;; Standard library -> compat shims\n    (:std/sugar        . (compat sugar))\n    (:std/format       . (compat format))\n    (:std/sort         . (compat sort))\n    (:std/pregexp      . (compat pregexp))\n    (:std/error        . (runtime error))\n    (:std/misc/string  . (compat misc))\n    (:std/misc/list    . (compat misc))\n    (:std/misc/hash    . (compat misc))\n\n    ;; Strip imports that have no Chez equivalent\n    (:std/iter         . #f)   ;; jerboa compiles for-loops natively\n    (:std/foreign      . #f)   ;; no FFI at this level\n    (:gerbil/core      . #f)   ;; runtime provides these\n    (:gerbil/runtime   . #f)\n\n    ;; Project-internal imports (relative)\n    (\"./util\"          . (mylib util))\n\n    ;; Cross-project dependencies\n    (:ober/aws/s3      . (jerboa-aws s3-api))\n    ))\n```\n\nThree mapping types:\n- **`(library name)`**: Replace the import with this R6RS library\n- **`#f`**: Strip the import entirely (provided by base imports or unnecessary)\n- Unmapped imports cause a compile error \u2014 this is intentional, so you catch\n  missing mappings early\n\n## Step 3: Define Base Imports\n\nBase imports are injected into every compiled module. They provide the runtime\nenvironment that Gerbil code expects:\n\n```scheme\n(define my-base-imports\n  '(;; Chez Scheme with exclusions for Gambit-compatible replacements\n    (except (chezscheme) void box box? unbox set-box!\n            andmap ormap iota last-pair find\n            1+ 1- fx/ fx1+ fx1-\n            error error? raise with-exception-handler identifier?\n            hash-table? make-hash-table)\n    ;; Jerboa runtime\n    (compat types)\n    (runtime util)\n    (runtime table)\n    (runtime mop)\n    (runtime error)\n    (runtime hash)\n    ;; Gambit compatibility (threading, u8vectors, etc.)\n    (except (compat gambit) number->string make-mutex\n            with-output-to-string)))\n```\n\nThe `(except (chezscheme) ...)` clause is critical. Jerboa's runtime\nredefines several Chez builtins with Gambit-compatible versions. Without\nthe exclusions, R6RS reports import conflicts.\n\n## Step 4: Write build-jerboa.ss\n\nThe complete translation driver:\n\n```scheme\n#!chezscheme\n(import\n  (except (chezscheme) void box box? unbox set-box!\n          andmap ormap iota last-pair find\n          1+ 1- fx/ fx1+ fx1-\n          error error? raise with-exception-handler identifier?\n          hash-table? make-hash-table)\n  (compiler compile))\n\n(define output-dir \"src/mylib\")\n\n;; Import map and base imports (as defined above)\n(define my-import-map '(...))\n(define my-base-imports '(...))\n\n;; Compile one module\n(define (compile-module name)\n  (let* ((input  (string-append \"my-gerbil-project/\" name \".ss\"))\n         (output (string-append output-dir \"/\" name \".sls\"))\n         (lib    `(mylib ,(string->symbol name))))\n    (printf \"  Compiling ~a.ss~n\" name)\n    (guard (exn\n             (#t (printf \"  ERROR: ~a: ~a~n\" name (condition-message exn))\n                 #f))\n      (let* ((lib-form (gerbil-compile-to-library\n                         input lib my-import-map my-base-imports))\n             (lib-form (fix-import-conflicts lib-form)))\n        (call-with-output-file output\n          (lambda (port)\n            (display \"#!chezscheme\\n\" port)\n            (parameterize ([print-gensym #f])\n              (pretty-print lib-form port)))\n          'replace)\n        (printf \"  OK: ~a~n\" output)\n        #t))))\n\n;; Compile in dependency order (leaves first)\n(display \"=== Translating .ss -> .sls ===\\n\")\n(compile-module \"util\")\n(compile-module \"core\")\n(compile-module \"main\")\n\n(display \"=== Translation complete ===\\n\")\n```\n\n### Key functions from `(compiler compile)`:\n\n- **`gerbil-compile-to-library`** `(input-path lib-name import-map base-imports)`\n  Reads a `.ss` file, compiles it through jerboa, wraps in an R6RS library form.\n\n- **`fix-import-conflicts`** `(lib-form)`\n  Post-processes the library to add `(except ...)` for local definitions that\n  shadow imports. Also fixes `set!`'d exports with `identifier-syntax`.\n\n- **`jerboa-compile-file`** `(input-path)`\n  Lower-level: compiles a file and returns the compiled forms (no library wrapper).\n\n- **`jerboa-compile-string`** `(code-string)`\n  Compiles a string of Gerbil code. Useful for testing.\n\n## Step 5: Write build-all.ss\n\nThis simply imports every module, which triggers Chez's `--compile-imported-libraries`:\n\n```scheme\n#!chezscheme\n(import (mylib util) (mylib core) (mylib main))\n(printf \"All modules compiled.~n\")\n```\n\n## Step 6: Write Compatibility Shims\n\nYour project likely imports Gerbil standard library modules (`:std/sugar`,\n`:std/format`, etc.). These need Chez equivalents in `src/compat/`.\n\n### Example: src/compat/sugar.sls\n\n```scheme\n#!chezscheme\n(library (compat sugar)\n  (export try catch finally with-catch\n          hash hash-ref hash-set! hash-update! hash-keys hash-values\n          defstruct defrules)\n  (import (chezscheme))\n\n  (define-syntax try\n    (syntax-rules (catch finally)\n      [(_ body (catch (var) handler ...))\n       (guard (var [#t handler ...]) body)]\n      [(_ body (finally cleanup ...))\n       (dynamic-wind void (lambda () body) (lambda () cleanup ...))]))\n\n  ;; ... more shims\n  )\n```\n\n### Common Compat Modules\n\n| Gerbil Module | Chez Equivalent | What It Provides |\n|---------------|-----------------|------------------|\n| `:std/sugar` | `(compat sugar)` | `try/catch/finally`, `hash` literals, `with-catch` |\n| `:std/format` | `(compat format)` | `fprintf`, `printf`, `format` (Gambit-style) |\n| `:std/sort` | `(compat sort)` | `sort`, `stable-sort` (Gambit arg order) |\n| `:std/pregexp` | `(compat pregexp)` | Portable regex (pure Scheme) |\n| `:std/misc/*` | `(compat misc)` | `string-split`, `string-join`, `path-expand`, etc. |\n| `:std/error` | `(runtime error)` | `Error`, `error?`, `with-exception-catcher` |\n| `:std/os/signal` | `(compat signal)` | Signal handling |\n| `:std/os/fdio` | `(compat fdio)` | File descriptor I/O |\n\nYou don't always need to write these from scratch \u2014 many shims follow a common pattern and can be adapted.\n\n## Step 7: Build and Test\n\n```bash\nmake              # translate + compile\nmake run          # run interpreted\nmake build        # build standalone binary (if build-binary.ss exists)\n```\n\n## Common Issues and Fixes\n\n### Import Conflicts\n\n**Problem**: R6RS forbids a local `define` that shadows an imported name.\n\n**Fix**: `fix-import-conflicts` handles this automatically. If you see\n\"multiple definitions\" errors, ensure you're calling it on the library form\nbefore writing it out.\n\n### set! on Exported Variables\n\n**Problem**: R6RS forbids `set!` on exported variables.\n\n**Fix**: `fix-import-conflicts` rewrites these using `identifier-syntax` with\na vector cell:\n```scheme\n;; Before (forbidden in R6RS):\n(export foo)\n(define foo 0)\n(set! foo 42)\n\n;; After (automatic rewrite):\n(export foo)\n(define foo-cell (vector 0))\n(define-syntax foo\n  (identifier-syntax\n    (id (vector-ref foo-cell 0))\n    ((set! id v) (vector-set! foo-cell 0 v))))\n```\n\n### Keyword Arguments\n\n**Problem**: Gerbil uses `key: value` keyword arguments. Jerboa translates\nthese, but `defclass` constructors may pass keyword values as positional args.\n\n**Fix**: Post-build patch in `build-jerboa.ss`:\n```scheme\n(patch-file! \"src/mylib/types.sls\"\n  \"(lp (cddr rest) (cons (cadr rest) acc))]\"\n  \"(lp (cddr rest) acc)]\")\n```\n\n### make-mutex String Arguments\n\n**Problem**: Gambit's `make-mutex` accepts strings; Chez requires symbols.\n\n**Fix**: Post-build patch:\n```scheme\n(patch-file! \"src/mylib/foo.sls\"\n  \"(make-mutex \\\"my-lock\\\")\"\n  \"(make-mutex 'my-lock)\")\n```\n\n### void Recursion\n\n**Problem**: Gerbil defines `(define (void) ...)` which creates a recursive\ndefinition in the translated output.\n\n**Fix**: Post-build patch:\n```scheme\n(patch-file! \"src/mylib/foo.sls\"\n  \"(define (void) (void))\"\n  \"(define (void . _) (if #f #f))\")\n```\n\n### Chez Lazy Library Invocation\n\n**Problem**: Chez only invokes a library when one of its exports is first\nreferenced at runtime. If a module registers side effects at load time (like\nregistering builtins), they won't execute.\n\n**Fix**: Add a dummy reference in a module that's guaranteed to be invoked:\n```scheme\n;; In main.sls, force invocation of side-effecting modules:\n(define _force-builtins special-builtin?)  ;; references a builtins export\n```\n\n### Threading\n\n**Problem**: Programs in Chez boot files cannot create threads (GC futex\ndeadlock).\n\n**Fix**: Put only libraries in the boot file. Load the program at runtime via\n`memfd` + `Sscheme_script`. See [docs/single-binary.md](single-binary.md).\n\n## FFI (C Bindings)\n\nFor projects that need C FFI:\n\n1. Write a C shim (`ffi-shim.c`) with the functions you need\n2. Compile to a shared library: `gcc -shared -fPIC -o libffi.so ffi-shim.c`\n3. Create `src/compat/ffi.sls` using Chez's `foreign-procedure` and `load-shared-object`\n4. Map Gerbil's FFI imports to your compat module\n\nFor standalone binaries, the C shim is compiled directly into the binary. See\n[docs/single-binary.md](single-binary.md).\n\n## Optimization\n\nFor production builds:\n\n```bash\nmake compile-opt3     # optimize-level 3\nmake compile-wpo      # whole-program optimization\n```\n\nSee [docs/optimization.md](optimization.md) for tuning `cp0-effort-limit`,\nper-file directives, and benchmark results.\n\n## Reference: Existing Projects\n\nStudy these for real-world patterns:\n\n| Project | Modules | Key Patterns |\n|---------|---------|--------------|\n| jerboa-kunabi | 10+ | Cross-project deps (jerboa-aws), sed-based patches |\n| jerboa-lsp | 53 | Large module count, JSON/HTTP compat |\n"}
+{"text": "<!-- FILE: docs/compiling-gerbil-projects.md -->\n# Compiling Gerbil Projects with Jerboa\n\nThis guide walks through the complete process of taking an existing Gerbil\nScheme project and compiling it to run on Chez Scheme via Jerboa.\n\n## Prerequisites\n\n1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://git.sr.ht/~lisp/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```\n\n## Project Layout\n\nA jerboa project wraps an existing Gerbil project with build scripts and a\ncompatibility layer:\n\n```\nmy-jerboa-project/\n  my-gerbil-project/        # Gerbil source (submodule or copy)\n    gerbil.pkg\n    *.ss                     # Gerbil source files (never modified)\n  src/\n    compat/                  # Chez compatibility shims (handwritten)\n      sugar.sls              # :std/sugar equivalents\n      format.sls             # :std/format equivalents\n      ...\n    mylib/                   # Generated .sls files (gitignored)\n  build-jerboa.ss            # Jerboa translation driver\n  build-all.ss               # Chez compilation trigger\n  build-binary.ss            # Optional: standalone binary builder\n  main.ss                    # Entry point program\n  Makefile\n```\n\n## Step 1: Create the Makefile\n\n```makefile\nSCHEME = scheme\nJERBOA = $(or $(JERBOA_DIR),$(HOME)/src/jerboa/src)\nLIBDIRS = src:$(JERBOA)\nCOMPILE = $(SCHEME) -q --libdirs $(LIBDIRS) --compile-imported-libraries\n\n.PHONY: all jerboa compile binary run clean\n\nall: jerboa compile\n\n# Translate .ss -> .sls via jerboa\njerboa:\n\t$(COMPILE) < build-jerboa.ss\n\n# Compile .sls -> .so via Chez\ncompile: jerboa\n\t$(COMPILE) < build-all.ss\n\n# Build standalone binary\nbuild: binary\nbinary: clean jerboa\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program build-binary.ss\n\n# Run interpreted\nrun: all\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program main.ss\n\nclean:\n\tfind src -name '*.so' -o -name '*.wpo' | xargs rm -f 2>/dev/null || true\n\trm -f src/mylib/*.sls\n```\n\nThe `JERBOA` variable points to jerboa's `src/` directory. Override it:\n```bash\nJERBOA_DIR=/path/to/jerboa/src make\n```\n\nIf your project depends on other jerboa-compiled libraries, add them to\n`LIBDIRS`:\n```makefile\nJERBOA_AWS = $(or $(JERBOA_AWS_DIR),jerboa-aws/src)\nLIBDIRS = src:$(JERBOA):$(JERBOA_AWS)\n```\n\n## Step 2: Write the Import Map\n\nThe import map is the most important part. It tells jerboa how to translate\nGerbil's `:module/path` imports into R6RS `(library name)` imports.\n\n```scheme\n(define my-import-map\n  '(;; Standard library -> compat shims\n    (:std/sugar        . (compat sugar))\n    (:std/format       . (compat format))\n    (:std/sort         . (compat sort))\n    (:std/pregexp      . (compat pregexp))\n    (:std/error        . (runtime error))\n    (:std/misc/string  . (compat misc))\n    (:std/misc/list    . (compat misc))\n    (:std/misc/hash    . (compat misc))\n\n    ;; Strip imports that have no Chez equivalent\n    (:std/iter         . #f)   ;; jerboa compiles for-loops natively\n    (:std/foreign      . #f)   ;; no FFI at this level\n    (:gerbil/core      . #f)   ;; runtime provides these\n    (:gerbil/runtime   . #f)\n\n    ;; Project-internal imports (relative)\n    (\"./util\"          . (mylib util))\n\n    ;; Cross-project dependencies\n    (:ober/aws/s3      . (jerboa-aws s3-api))\n    ))\n```\n\nThree mapping types:\n- **`(library name)`**: Replace the import with this R6RS library\n- **`#f`**: Strip the import entirely (provided by base imports or unnecessary)\n- Unmapped imports cause a compile error \u2014 this is intentional, so you catch\n  missing mappings early\n\n## Step 3: Define Base Imports\n\nBase imports are injected into every compiled module. They provide the runtime\nenvironment that Gerbil code expects:\n\n```scheme\n(define my-base-imports\n  '(;; Chez Scheme with exclusions for Gambit-compatible replacements\n    (except (chezscheme) void box box? unbox set-box!\n            andmap ormap iota last-pair find\n            1+ 1- fx/ fx1+ fx1-\n            error error? raise with-exception-handler identifier?\n            hash-table? make-hash-table)\n    ;; Jerboa runtime\n    (compat types)\n    (runtime util)\n    (runtime table)\n    (runtime mop)\n    (runtime error)\n    (runtime hash)\n    ;; Gambit compatibility (threading, u8vectors, etc.)\n    (except (compat gambit) number->string make-mutex\n            with-output-to-string)))\n```\n\nThe `(except (chezscheme) ...)` clause is critical. Jerboa's runtime\nredefines several Chez builtins with Gambit-compatible versions. Without\nthe exclusions, R6RS reports import conflicts.\n\n## Step 4: Write build-jerboa.ss\n\nThe complete translation driver:\n\n```scheme\n#!chezscheme\n(import\n  (except (chezscheme) void box box? unbox set-box!\n          andmap ormap iota last-pair find\n          1+ 1- fx/ fx1+ fx1-\n          error error? raise with-exception-handler identifier?\n          hash-table? make-hash-table)\n  (compiler compile))\n\n(define output-dir \"src/mylib\")\n\n;; Import map and base imports (as defined above)\n(define my-import-map '(...))\n(define my-base-imports '(...))\n\n;; Compile one module\n(define (compile-module name)\n  (let* ((input  (string-append \"my-gerbil-project/\" name \".ss\"))\n         (output (string-append output-dir \"/\" name \".sls\"))\n         (lib    `(mylib ,(string->symbol name))))\n    (printf \"  Compiling ~a.ss~n\" name)\n    (guard (exn\n             (#t (printf \"  ERROR: ~a: ~a~n\" name (condition-message exn))\n                 #f))\n      (let* ((lib-form (gerbil-compile-to-library\n                         input lib my-import-map my-base-imports))\n             (lib-form (fix-import-conflicts lib-form)))\n        (call-with-output-file output\n          (lambda (port)\n            (display \"#!chezscheme\\n\" port)\n            (parameterize ([print-gensym #f])\n              (pretty-print lib-form port)))\n          'replace)\n        (printf \"  OK: ~a~n\" output)\n        #t))))\n\n;; Compile in dependency order (leaves first)\n(display \"=== Translating .ss -> .sls ===\\n\")\n(compile-module \"util\")\n(compile-module \"core\")\n(compile-module \"main\")\n\n(display \"=== Translation complete ===\\n\")\n```\n\n### Key functions from `(compiler compile)`:\n\n- **`gerbil-compile-to-library`** `(input-path lib-name import-map base-imports)`\n  Reads a `.ss` file, compiles it through jerboa, wraps in an R6RS library form.\n\n- **`fix-import-conflicts`** `(lib-form)`\n  Post-processes the library to add `(except ...)` for local definitions that\n  shadow imports. Also fixes `set!`'d exports with `identifier-syntax`.\n\n- **`jerboa-compile-file`** `(input-path)`\n  Lower-level: compiles a file and returns the compiled forms (no library wrapper).\n\n- **`jerboa-compile-string`** `(code-string)`\n  Compiles a string of Gerbil code. Useful for testing.\n\n## Step 5: Write build-all.ss\n\nThis simply imports every module, which triggers Chez's `--compile-imported-libraries`:\n\n```scheme\n#!chezscheme\n(import (mylib util) (mylib core) (mylib main))\n(printf \"All modules compiled.~n\")\n```\n\n## Step 6: Write Compatibility Shims\n\nYour project likely imports Gerbil standard library modules (`:std/sugar`,\n`:std/format`, etc.). These need Chez equivalents in `src/compat/`.\n\n### Example: src/compat/sugar.sls\n\n```scheme\n#!chezscheme\n(library (compat sugar)\n  (export try catch finally with-catch\n          hash hash-ref hash-set! hash-update! hash-keys hash-values\n          defstruct defrules)\n  (import (chezscheme))\n\n  (define-syntax try\n    (syntax-rules (catch finally)\n      [(_ body (catch (var) handler ...))\n       (guard (var [#t handler ...]) body)]\n      [(_ body (finally cleanup ...))\n       (dynamic-wind void (lambda () body) (lambda () cleanup ...))]))\n\n  ;; ... more shims\n  )\n```\n\n### Common Compat Modules\n\n| Gerbil Module | Chez Equivalent | What It Provides |\n|---------------|-----------------|------------------|\n| `:std/sugar` | `(compat sugar)` | `try/catch/finally`, `hash` literals, `with-catch` |\n| `:std/format` | `(compat format)` | `fprintf`, `printf`, `format` (Gambit-style) |\n| `:std/sort` | `(compat sort)` | `sort`, `stable-sort` (Gambit arg order) |\n| `:std/pregexp` | `(compat pregexp)` | Portable regex (pure Scheme) |\n| `:std/misc/*` | `(compat misc)` | `string-split`, `string-join`, `path-expand`, etc. |\n| `:std/error` | `(runtime error)` | `Error`, `error?`, `with-exception-catcher` |\n| `:std/os/signal` | `(compat signal)` | Signal handling |\n| `:std/os/fdio` | `(compat fdio)` | File descriptor I/O |\n\nYou don't always need to write these from scratch \u2014 many shims follow a common pattern and can be adapted.\n\n## Step 7: Build and Test\n\n```bash\nmake              # translate + compile\nmake run          # run interpreted\nmake build        # build standalone binary (if build-binary.ss exists)\n```\n\n## Common Issues and Fixes\n\n### Import Conflicts\n\n**Problem**: R6RS forbids a local `define` that shadows an imported name.\n\n**Fix**: `fix-import-conflicts` handles this automatically. If you see\n\"multiple definitions\" errors, ensure you're calling it on the library form\nbefore writing it out.\n\n### set! on Exported Variables\n\n**Problem**: R6RS forbids `set!` on exported variables.\n\n**Fix**: `fix-import-conflicts` rewrites these using `identifier-syntax` with\na vector cell:\n```scheme\n;; Before (forbidden in R6RS):\n(export foo)\n(define foo 0)\n(set! foo 42)\n\n;; After (automatic rewrite):\n(export foo)\n(define foo-cell (vector 0))\n(define-syntax foo\n  (identifier-syntax\n    (id (vector-ref foo-cell 0))\n    ((set! id v) (vector-set! foo-cell 0 v))))\n```\n\n### Keyword Arguments\n\n**Problem**: Gerbil uses `key: value` keyword arguments. Jerboa translates\nthese, but `defclass` constructors may pass keyword values as positional args.\n\n**Fix**: Post-build patch in `build-jerboa.ss`:\n```scheme\n(patch-file! \"src/mylib/types.sls\"\n  \"(lp (cddr rest) (cons (cadr rest) acc))]\"\n  \"(lp (cddr rest) acc)]\")\n```\n\n### make-mutex String Arguments\n\n**Problem**: Gambit's `make-mutex` accepts strings; Chez requires symbols.\n\n**Fix**: Post-build patch:\n```scheme\n(patch-file! \"src/mylib/foo.sls\"\n  \"(make-mutex \\\"my-lock\\\")\"\n  \"(make-mutex 'my-lock)\")\n```\n\n### void Recursion\n\n**Problem**: Gerbil defines `(define (void) ...)` which creates a recursive\ndefinition in the translated output.\n\n**Fix**: Post-build patch:\n```scheme\n(patch-file! \"src/mylib/foo.sls\"\n  \"(define (void) (void))\"\n  \"(define (void . _) (if #f #f))\")\n```\n\n### Chez Lazy Library Invocation\n\n**Problem**: Chez only invokes a library when one of its exports is first\nreferenced at runtime. If a module registers side effects at load time (like\nregistering builtins), they won't execute.\n\n**Fix**: Add a dummy reference in a module that's guaranteed to be invoked:\n```scheme\n;; In main.sls, force invocation of side-effecting modules:\n(define _force-builtins special-builtin?)  ;; references a builtins export\n```\n\n### Threading\n\n**Problem**: Programs in Chez boot files cannot create threads (GC futex\ndeadlock).\n\n**Fix**: Put only libraries in the boot file. Load the program at runtime via\n`memfd` + `Sscheme_script`. See [docs/single-binary.md](single-binary.md).\n\n## FFI (C Bindings)\n\nFor projects that need C FFI:\n\n1. Write a C shim (`ffi-shim.c`) with the functions you need\n2. Compile to a shared library: `gcc -shared -fPIC -o libffi.so ffi-shim.c`\n3. Create `src/compat/ffi.sls` using Chez's `foreign-procedure` and `load-shared-object`\n4. Map Gerbil's FFI imports to your compat module\n\nFor standalone binaries, the C shim is compiled directly into the binary. See\n[docs/single-binary.md](single-binary.md).\n\n## Optimization\n\nFor production builds:\n\n```bash\nmake compile-opt3     # optimize-level 3\nmake compile-wpo      # whole-program optimization\n```\n\nSee [docs/optimization.md](optimization.md) for tuning `cp0-effort-limit`,\nper-file directives, and benchmark results.\n\n## Reference: Existing Projects\n\nStudy these for real-world patterns:\n\n| Project | Modules | Key Patterns |\n|---------|---------|--------------|\n| jerboa-kunabi | 10+ | Cross-project deps (jerboa-aws), sed-based patches |\n| jerboa-lsp | 53 | Large module count, JSON/HTTP compat |\n"}
 {"text": "<!-- FILE: docs/optimization.md -->\n# Chez Scheme Optimization Techniques for Jerboa\n\nTechniques from the Chez Scheme compiler (v10.4.0) that can improve performance of Gerbil-to-Chez converted code.\n\n## 1. Compiler Parameter Tuning\n\nThe most impactful knobs for compiled Jerboa output:\n\n| Parameter | Default | Recommendation |\n|-----------|---------|----------------|\n| `optimize-level` | 2 | Use 3 for well-tested code (removes all type checks) |\n| `cp0-effort-limit` | 200 | Increase to 500+ for generated code with deep nesting |\n| `cp0-score-limit` | 20 | Increase to 50 to allow more aggressive inlining |\n| `enable-type-recovery` | #t | Keep enabled \u2014 helps recover types after Gerbil wrapping layers |\n| `enable-cross-library-optimization` | #t | Critical since Jerboa emits many small R6RS libraries |\n\nSet these per-file in the compilation pipeline via `(eval-when (compile) ...)` for hot modules.\n\n## 2. Record Type Flags\n\nWhen compiling `defstruct`/`defclass` to Chez records, emit:\n\n- **`(sealed #t)`** \u2014 prevents subclassing, enables direct field access without vtable indirection\n- **`(immutable)` on fields** where Gerbil code never uses `set!` on slots \u2014 cp0 can then fold field accesses\n- **`(nongenerative <uid>)`** \u2014 enables cross-library type identity and optimization\n\nCurrently the MOP uses `gerbil-struct` wrappers. For performance-critical structs, consider emitting native Chez `define-record-type` directly with these flags.\n\n## 3. Numeric Specialization\n\nChez unboxes flonums at level 3 but only with explicit flonum ops:\n\n```scheme\n;; Generic (boxes intermediate results, GC pressure)\n(+ (* x y) z)\n\n;; Specialized (raw FPU instructions, no allocation)\n(fl+ (fl* x y) z)\n```\n\nThe compiler could emit `fx+`/`fl+` when type annotations are available from Gerbil's `def` forms (e.g., `(def (f (x : fixnum)) ...)`).\n\n## 4. Local Binding Optimization\n\nChez's cp0 aggressively inlines **locally-bound, unassigned procedures**. Top-level `define`s across library boundaries are harder to optimize. This matters because Jerboa emits many small libraries.\n\n**Actionable**: When a Gerbil helper function is only used in one place, emit it as a local `let` binding rather than a top-level `define`.\n\n## 5. Type Guard Patterns\n\ncp0 + cptypes recognize predicate-guarded branches:\n\n```scheme\n;; cptypes knows x is a pair in the consequent\n(if (pair? x) (car x) ...)\n\n;; cptypes knows x is a fixnum\n(if (fixnum? x) (fx+ x 1) ...)\n```\n\nThe `match` compiler already emits type tests \u2014 make sure the pattern is `(if (predicate? x) <then> <else>)` rather than intermediate wrappers that obscure the type information from cptypes.\n\n## 6. Avoid the WPO Trap\n\nWPO breaks `identifier-syntax` mutable export cells. But for pure-Scheme modules without mutable exports, WPO could still be applied selectively. Consider a build flag that enables WPO per-module.\n\n## 7. Letrec Decomposition\n\nChez's `cpletrec` pass decomposes `letrec*` into simpler forms. Since Gerbil's top-level `def` forms compile to `letrec*` bodies in R6RS libraries, this pass is already working for you. But ordering matters \u2014 put pure definitions before impure ones to help cpletrec separate them.\n\n## 8. Primitive Inlining Flags\n\nEach Chez primitive has optimization flags (`pure`, `unrestricted`, `cp02`, `arith-op`, etc.) in `primdata.ss`. When the compat layer wraps Chez primitives (e.g., `hash-ref` wrapping `hashtable-ref`), the wrapper obscures these flags from the optimizer.\n\n**Actionable**: For hot-path compat wrappers, consider:\n- Using `define-syntax` + `syntax-rules` instead of `define` to make them transparent to cp0\n- Or mark them with integration info if applicable\n\n## 9. Closure Avoidance in Loops\n\nChez doesn't optimize closures allocated inside loops. The `for`/`for/collect` compilation should ensure lambda bodies are hoisted when they don't capture loop variables.\n\n## 10. Quick Wins Checklist\n\n- [ ] Add `(optimize-level 3)` option to gxc for release builds\n- [ ] Emit `sealed` records for `defstruct` without known subclasses\n- [ ] Emit `immutable` fields for non-mutated struct slots\n- [ ] Emit `fx+`/`fx-` for integer arithmetic in type-annotated code\n- [ ] Ensure `match` type tests are direct `(if (pred? x) ...)` \u2014 not wrapped\n- [ ] Increase `cp0-effort-limit` for generated code (deep nesting from macro expansion)\n- [ ] Consider selective WPO for pure modules without mutable exports\n- [ ] Profile the compat wrappers (hash, MOP dispatch) \u2014 these are likely the hottest paths\n\n---\n\n## 11. Chez Scheme Optimization Functions & APIs\n\n### `expand/optimize` \u2014 Inspect Optimizer Output\n\nUse this to see what Chez's optimizer actually does with generated code. Invaluable for checking whether compat wrappers are being inlined or blocking optimization:\n\n```scheme\n(expand/optimize '(let ([y '(3 . 4)]) (+ (car y) (cdr y))))\n;=> 7   (fully folded at compile time)\n\n(print-gensym #f)\n(expand/optimize\n  '(let ([y '(3 . 4)])\n     (lambda (x) (* (+ (car y) (cdr y)) x))))\n;=> (lambda (x) (#2%* 7 x))\n```\n\n### `compile-whole-program` \u2014 Whole-Program Optimization\n\nThe most aggressive optimization Chez offers. Discards unused code and optimizes across all library boundaries:\n\n```scheme\n(parameterize ([generate-wpo-files #t])\n  (compile-program \"main.ss\" \"main.so\"))\n(compile-whole-program \"main.wpo\" \"main-opt.so\")\n```\n\nReturns a list of libraries that couldn't be incorporated (missing `.wpo` files). Breaks `identifier-syntax` mutable export cells \u2014 use only for modules without mutable exports.\n\n### `compile-whole-library` \u2014 Same for Libraries\n\nLike `compile-whole-program` but for library bundles. Produces both `.so` and `.wpo` output.\n\n### `$primitive` / `#%` Syntax \u2014 Force Original Primitives\n\nBypass any user redefinitions and control per-call optimization level:\n\n```scheme\n(#3%car x)     ; car at optimize-level 3 (no type check)\n(#2%+ a b)     ; + at optimize-level 2 (with checks)\n(#%vector-ref v i)  ; original primitive, current optimize level\n```\n\n## 12. Complete Parameter Reference\n\n| Parameter | Default | Effect |\n|-----------|---------|--------|\n| `optimize-level` | 0 | 0-2 = safe; **3 = unsafe** (no type/bounds checks) |\n| `run-cp0` | 2-pass | Controls the source optimizer; can disable or run N passes |\n| `cp0-effort-limit` | 200 | Max work per inlining attempt |\n| `cp0-score-limit` | 20 | Max code growth per inlining |\n| `cp0-outer-unroll-limit` | 0 | Loop unrolling (set to 1+ to enable) |\n| `enable-type-recovery` | #t | cptypes pass \u2014 removes redundant type checks |\n| `enable-cross-library-optimization` | #t | Propagate constants/inlining across libraries |\n| `enable-unsafe-application` | #f | Skip procedure? check on calls (even below level 3) |\n| `enable-unsafe-variable-reference` | #f | Skip letrec undefined-var checks |\n| `enable-arithmetic-left-associative` | #f | Allow arithmetic reassociation |\n| `commonization-level` | 0 | 0-9; merges structurally similar lambdas (reduces code size) |\n| `generate-wpo-files` | #f | Emit `.wpo` files for whole-program optimization |\n| `generate-inspector-information` | #t | Set #f to reduce object file size |\n| `debug-level` | 1 | 0 = max optimization, 3 = max debuggability |\n| `compile-interpret-simple` | #t | Interpret simple expressions instead of compiling |\n\n## 13. Customizing the cp0 Pass\n\nThe default runs cp0 **twice** (second pass catches things the first missed after inlining). Customizable via `run-cp0`:\n\n```scheme\n;; Disable cp0 entirely (fastest compile, no optimization)\n(run-cp0 (lambda (cp0 x) x))\n\n;; Run once (faster compile, slightly less optimization)\n(run-cp0 (lambda (cp0 x) (cp0 x)))\n\n;; Run 3 times (slower compile, more aggressive)\n(run-cp0 (lambda (cp0 x) (cp0 (cp0 (cp0 x)))))\n```\n\n## 14. Commonization \u2014 Code Size Reduction for Generated Code\n\nThe `commonization-level` parameter (0-9) merges structurally similar lambda expressions by abstracting differences at leaf positions (constants, variable references, primitive references).\n\n```scheme\n(eval-when (compile) (commonization-level 5))\n```\n\n**Especially relevant for Jerboa**: the compiler generates many structurally similar lambdas (match arms, method dispatchers, accessor functions). Setting this to 3-5 could significantly reduce code size and improve instruction cache utilization. Trade-off: can undo some cp0 inlining and complicate debugging.\n\n## 15. Per-File Optimization Directives\n\nApply optimization settings to individual files without affecting the rest of the build:\n\n```scheme\n(eval-when (compile)\n  (optimize-level 3)\n  (cp0-effort-limit 500)\n  (generate-inspector-information #f)\n  (commonization-level 3))\n```\n\nThis only affects compilation (via `compile-file`), not source loading.\n\n## 16. Recommendations Specific to Jerboa\n\n1. **`expand/optimize`** \u2014 Use to inspect whether compat wrappers (hash-ref, slot-ref) are being inlined or blocking optimization. If they're opaque, convert to `define-syntax` wrappers.\n\n2. **`commonization-level` 3-5** \u2014 High value for Jerboa since the compiler generates many structurally similar lambdas from match, defmethod, and accessor compilation.\n\n3. **`compile-whole-program`** \u2014 For modules without mutable exports, this gives the biggest wins. Worth investigating per-module WPO enablement.\n\n4. **`enable-unsafe-application`** \u2014 A middle ground: skip the \"is this a procedure?\" check without going full optimize-level 3. Safe for generated code where every call target is known to be a procedure.\n\n5. **`generate-inspector-information #f`** \u2014 For release builds, disabling this reduces `.so` file sizes.\n\n6. **`debug-level 0`** \u2014 For release builds, allows maximum continuation optimization.\n\n## 17. Empirical Results (gherkin-shell benchmarks)\n\nTested on gherkin-shell using shellbench. All numbers are executions/second (higher = better).\n\n| Optimization | Avg Improvement | Best Test | Binary Size |\n|-------------|----------------|-----------|-------------|\n| Baseline (default) | \u2014 | \u2014 | 6,543 KB |\n| optimize-level 3 (shell only) | +3.2% | +6.3% | 6,499 KB |\n| opt3 (runtime + shell) | +5.2% | +6.4% | 6,475 KB |\n| opt3 + tuned cp0 + no inspector | +5.0% | +6.3% | 6,003 KB (-8.3%) |\n| opt3 + tuned cp0 + partial WPO | +8.7% | +16.3% | 7,075 KB |\n| **opt3 + tuned cp0 + full WPO** | **+9.6%** | **+20.3%** | 6,891 KB (+5.3%) |\n\nKey findings:\n- **WPO works** despite `identifier-syntax` mutable exports (they survived WPO in this project)\n- **Full WPO requires `.wpo` files** from both the Jerboa runtime and application compat layers\n- **cp0 tuning** (effort 500, score 50) adds ~1-2% on top of opt3 alone\n- **`generate-inspector-information #f`** reduces binary size significantly (-8.3%) with negligible performance impact\n- **Biggest wins** on comparison operations (`cmp: [ ]` +20.3%) and arithmetic (`count: typeset -i` +17.9%)\n\n## Summary\n\nThe biggest gains will come from **optimize-level 3** (+3-5%), **WPO** (+4-5% additional), and **tuned cp0 limits** (+1-2%). Record type flags (sealed/immutable) and commonization remain untested but should provide further gains. Use `expand/optimize` to verify that optimizations are actually taking effect on generated code.\n"}
 {"text": "<!-- FILE: docs/fix-fibers.md -->\n# Fiber Gaps \u2014 Implementation Plan\n\nStatus: **draft** (2026-04-11)\n\nThe `(std fiber)` core (668 lines, 18 tests) is solid: M:N scheduling, engine-based preemption, cooperative yield, fiber-aware channels, sleep with timer queue. This document covers the gaps and how to fill them, ordered by value.\n\n---\n\n## 1. Fix `cpu-count` (trivial)\n\n**Gap**: `fiber.sls:221-222` hardcodes `(if (threaded?) 4 1)`.\n\n**Fix**: Reuse the real detection from `actor/scheduler.sls:135-146`, which reads `/proc/cpuinfo` with a fallback to 4.\n\nOptions:\n- (a) Factor the scheduler's `cpu-count` into a shared `(std misc cpu)` module that both import.\n- (b) Inline the same `/proc/cpuinfo` reader into `fiber.sls`.\n\nPrefer (a) \u2014 one source of truth, and other modules (engine pool, task groups) can use it too.\n\n**Effort**: ~30 minutes. **Tests**: Verify `(make-fiber-runtime)` picks up actual core count.\n\n---\n\n## 2. Fiber-local storage\n\n**Gap**: No per-fiber variable bindings. `current-fiber` and `current-fiber-runtime` are `make-thread-parameter`, which is OS-thread-scoped. Since multiple fibers share one OS worker thread, a fiber-local needs its own mechanism.\n\n**Design**: A `fiber-parameter` that stores values keyed by fiber ID.\n\n```scheme\n(define (make-fiber-parameter default)\n  (let ([store (make-eq-hashtable)]\n        [mx (make-mutex)])\n    (case-lambda\n      [()    ;; read\n       (let ([f (current-fiber)])\n         (if f\n           (begin (mutex-acquire mx)\n                  (let ([v (hashtable-ref store (fiber-id f) default)])\n                    (mutex-release mx) v))\n           default))]\n      [(val) ;; write\n       (let ([f (current-fiber)])\n         (unless f (error 'fiber-parameter \"not in a fiber\"))\n         (mutex-acquire mx)\n         (hashtable-set! store (fiber-id f) val)\n         (mutex-release mx))])))\n```\n\n**Cleanup**: When a fiber completes (`mark-fiber-done!`), sweep its entries from all registered fiber-parameters. Keep a global weak list of all live fiber-parameters, or add a per-fiber cleanup hook list.\n\n**Convenience macro**:\n```scheme\n(define-syntax fiber-parameterize\n  (syntax-rules ()\n    [(_ ([fp val] ...) body ...)\n     (let ([old-fp (fp)] ...)\n       (dynamic-wind\n         (lambda () (fp val) ...)\n         (lambda () body ...)\n         (lambda () (fp old-fp) ...)))]))\n```\n\n**Effort**: ~2 hours. **Tests**: fiber-local isolation across concurrent fibers on same worker thread, cleanup after fiber completion.\n\n---\n\n## 3. Fiber cancellation\n\n**Gap**: No way to cancel a running fiber from outside. Once spawned, a fiber runs until its thunk returns or raises.\n\n**Design**: Cooperative cancellation via a cancel token (same pattern as `task.sls:36-53`).\n\nAdd to the fiber record:\n```scheme\n(mutable cancelled?)   ;; boolean, checked at yield/sleep/channel-wait\n```\n\nAPI:\n```scheme\n(fiber-cancel! f)          ;; set cancelled? flag, wake if parked\n(fiber-cancelled? f)       ;; check flag\n(fiber-check-cancelled!)   ;; called inside fiber \u2014 raises &fiber-cancelled if set\n```\n\n**Cancellation points**: `fiber-yield`, `fiber-sleep`, `fiber-channel-recv`, and `fiber-channel-send` all check the cancelled flag before parking. Raise `&fiber-cancelled` condition.\n\n**Force-cancel after timeout**: Like the actor supervisor's graceful shutdown (`supervisor.sls:215-240`), allow a deadline after which the fiber's engine is simply not resumed.\n\n```scheme\n(fiber-cancel! f)                ;; cooperative \u2014 sets flag\n(fiber-cancel! f timeout-ms)     ;; cooperative, then force-abandon after timeout\n```\n\n**Condition type**:\n```scheme\n(define-condition-type &fiber-cancelled &serious\n  make-fiber-cancelled fiber-cancelled?\n  (fiber-id cancelled-fiber-id))\n```\n\n**Effort**: ~3 hours. **Tests**: cancel parked fiber, cancel running fiber at yield point, cancel with timeout, double-cancel idempotent.\n\n---\n\n## 4. Error propagation\n\n**Gap**: Exceptions in a fiber are silently captured in `fiber-result` (`fiber.sls:436-441`). No notification to parent or supervisor.\n\n**Design**: Two mechanisms, matching the actor system's patterns.\n\n### 4a. fiber-join (blocking result retrieval)\n\n```scheme\n(fiber-join f)             ;; block current fiber until f completes, return result\n(fiber-join f timeout-ms)  ;; with timeout, raises &fiber-timeout on expiry\n```\n\nIf `f` completed with an exception, `fiber-join` re-raises it in the joining fiber. Implementation: add a \"join-waiters\" list to the fiber record; `mark-fiber-done!` wakes them.\n\n### 4b. fiber-link (Erlang-style crash propagation)\n\n```scheme\n(fiber-link! f)      ;; link current fiber to f \u2014 if f dies with error, current fiber gets &fiber-linked-crash\n(fiber-unlink! f)\n```\n\nWhen a linked fiber crashes, all linked fibers receive a `&fiber-linked-crash` condition at their next cancellation point. Simpler than full OTP monitors but covers the \"if my child dies, I die\" use case.\n\n**Condition types**:\n```scheme\n(define-condition-type &fiber-timeout &serious\n  make-fiber-timeout fiber-timeout?\n  (fiber-id timeout-fiber-id))\n\n(define-condition-type &fiber-linked-crash &serious\n  make-fiber-linked-crash fiber-linked-crash?\n  (source-fiber-id linked-crash-source)\n  (original-condition linked-crash-condition))\n```\n\n**Effort**: 4a ~2 hours, 4b ~3 hours. **Tests**: join on completed fiber, join on crashed fiber re-raises, join timeout, link propagation.\n\n---\n\n## 5. Fiber-channel select (`fiber-select`)\n\n**Gap**: No way to wait on multiple fiber-channels simultaneously.\n\n**Design**: Adapt the CSP select spin-poll pattern (`csp/select.sls:166-174`) for fiber-channels.\n\n```scheme\n(fiber-select\n  [ch1 val => (handle-val val)]           ;; recv from ch1\n  [ch2 :send msg => (handle-sent)]        ;; send msg to ch2\n  [:timeout 5000 => (handle-timeout)]     ;; optional timeout clause\n  [:default => (handle-none)])            ;; optional non-blocking clause\n```\n\n**Implementation**: Macro expands to a loop that:\n1. Try each clause's channel with `fiber-channel-try-recv` / `fiber-channel-try-send`.\n2. If any succeeds, evaluate its body and return.\n3. If `:default` clause exists and nothing ready, run it.\n4. Otherwise, park the fiber on all channels' waiter lists, use a shared gate so that whichever channel fires first wakes the fiber.\n\nThe shared-gate approach avoids the spin-poll cost: register the fiber on multiple channels, first one to wake it wins, others remove the stale waiter entry.\n\n**Alternative**: Integrate with `(std event)` by providing `fiber-recv-evt` / `fiber-send-evt` that return event objects compatible with `choice` and `sync`. This is cleaner but couples the two modules.\n\nRecommend: start with the macro (self-contained in `(std fiber)`), add event integration later.\n\n**Effort**: ~4 hours. **Tests**: select across 2+ channels, select with timeout, select with default, select with send+recv mix.\n\n---\n\n## 6. Structured concurrency (`with-fiber-group`)\n\n**Gap**: No scoped lifecycle management. Fibers spawned with `fiber-spawn*` are fire-and-forget; no automatic cleanup, no \"wait for all children\" scope.\n\n**Design**: Follows the pattern from `task.sls` and `concur/structured.sls`.\n\n```scheme\n(with-fiber-group\n  (lambda (group)\n    (fiber-group-spawn group (lambda () (do-work-a)))\n    (fiber-group-spawn group (lambda () (do-work-b)))\n    ;; implicit: waits for all children to complete\n    ;; if any child raises, cancels siblings, re-raises in parent\n    ))\n```\n\n**Semantics**:\n- `with-fiber-group` creates a group, evaluates the body, then blocks until all spawned fibers complete.\n- If any fiber in the group raises an unhandled exception, all other fibers in the group are cancelled (cooperative), and the exception is re-raised in the calling fiber.\n- If the calling fiber is itself cancelled, all children are cancelled.\n- Cleanup is guaranteed via `dynamic-wind`.\n\n**Group record**:\n```scheme\n(define-record-type fiber-group\n  (fields\n    (mutable fibers)        ;; list of child fibers\n    (mutable first-exn)     ;; first exception, or #f\n    (mutable cancelled?)\n    (immutable mutex)\n    (immutable all-done)    ;; condition variable\n    (mutable done-count)\n    (mutable total-count)))\n```\n\n**Effort**: ~4 hours. **Tests**: all-succeed, first-error-cancels-rest, parent-cancel-propagates, nested groups.\n\n---\n\n## 7. Fiber-aware timeouts\n\n**Gap**: `fiber-sleep` parks for a duration, but there's no \"do X or timeout\" pattern.\n\n**Design**: Build on `fiber-select` (gap 5) plus a timeout channel.\n\n```scheme\n(define (fiber-timeout ms)\n  ;; Returns a fiber-channel that receives (void) after ms milliseconds.\n  ;; Uses the existing timer-queue infrastructure.\n  (let ([ch (make-fiber-channel 1)]\n        [rt (current-fiber-runtime)])\n    (let* ([now (current-time 'time-utc)]\n           [deadline (add-duration now\n                       (make-time 'time-duration\n                                  (* (fxmod ms 1000) 1000000)\n                                  (fxquotient ms 1000)))])\n      ;; Spawn a tiny fiber that sleeps then sends\n      (fiber-spawn* (lambda ()\n        (fiber-sleep ms)\n        (fiber-channel-try-send ch (void)))))\n    ch))\n```\n\nUsage with `fiber-select`:\n```scheme\n(fiber-select\n  [work-ch result => (process result)]\n  [(fiber-timeout 5000) _ => (error 'timeout \"took too long\")])\n```\n\nBetter approach (no extra fiber): register directly on the timer queue, wake a channel when the deadline fires. Requires a small extension to the timer queue to fire arbitrary callbacks, not just wake fibers.\n\n**Effort**: ~1 hour (spawn approach), ~2 hours (timer-queue callback approach). **Tests**: timeout fires, timeout not needed (work completes first).\n\n---\n\n## Implementation Order\n\n| Priority | Gap | Effort | Dependencies |\n|----------|-----|--------|--------------|\n| 1 | cpu-count fix | 30 min | none |\n| 2 | Fiber cancellation | 3 hours | none |\n| 3 | Fiber-local storage | 2 hours | none |\n| 4 | Error propagation (fiber-join) | 2 hours | cancellation (for timeout variant) |\n| 5 | Fiber-channel select | 4 hours | none (but better with timeouts) |\n| 6 | Fiber-aware timeouts | 2 hours | select |\n| 7 | Structured concurrency | 4 hours | cancellation + error propagation |\n| 8 | Error propagation (fiber-link) | 3 hours | cancellation |\n\n**Total**: ~20 hours of implementation.\n\nGaps 1-4 are independent and can be done in any order. Gap 5 (select) unlocks gap 6 (timeouts). Gap 7 (structured concurrency) needs gaps 2 + 4.\n\n---\n\n## Existing Infrastructure to Reuse\n\n| Need | Source | Path |\n|------|--------|------|\n| Real CPU detection | actor scheduler | `lib/std/actor/scheduler.sls:135-146` |\n| Cancel tokens | task groups | `lib/std/task.sls:36-53` |\n| Supervision patterns | actor supervisor | `lib/std/actor/supervisor.sls` |\n| Spin-poll select | CSP select | `lib/std/csp/select.sls:166-174` |\n| Timer wheel | CSP select | `lib/std/csp/select.sls:299-410` |\n| Event abstraction | event system | `lib/std/event.sls:43-61` |\n| Error aggregation | task groups | `lib/std/task.sls:121-149` |\n| Hierarchical cleanup | custodians | `lib/std/misc/custodian.sls` |\n\n---\n\n## What's NOT in scope\n\n- **Work stealing**: The current run-queue is a single shared FIFO. A per-worker deque with stealing (like `actor/scheduler.sls`) would improve cache locality under heavy load, but the current design works well up to ~10K fibers. Optimize later if profiling shows contention.\n- **Fiber migration**: Moving a parked fiber from one worker to another. Engine continuations are not trivially portable across threads in Chez. Not needed for correctness, only for load balancing (which work-stealing would address).\n- **Async I/O integration**: Tying fiber parking to epoll/kqueue so that file/socket readiness wakes a fiber. This is a large project (essentially an event loop runtime) and orthogonal to the gaps above.\n"}
 {"text": "<!-- FILE: docs/pending.md -->\n# Pending: Making Jerboa Better\n\nLast updated: 2026-03-22.\n\n## Current State\n\n500+ modules, ~126K lines, 3,100+ tests, 13 fuzz harnesses. Full Gerbil API surface on stock Chez Scheme. Rust native backend (`libjerboa_native.so`) for crypto, compression, regex, databases, and OS integration. Legacy chez-* C FFI shims still available as fallbacks. A complete editor (jerboa-emacs) with TUI + Qt, Org-mode, LSP, Git.\n\n---\n\n## Language Completeness\n\n### chez-srfis \u2014 Standalone SRFI Repository\n\n35 SRFIs currently in `lib/std/srfi/`. Create `~/mine/chez-srfis` as a standalone repo (like `chez-r7rs`) and push toward 100+.\n\n**Priority SRFIs:**\n- SRFI-41 (streams) \u2014 standard API over lazy-seq\n- SRFI-64 (test framework)\n- SRFI-69 (hash tables) \u2014 older widely-used API\n- SRFI-152 (strings)\n- SRFI-170 (POSIX API) \u2014 complement `(std os *)`\n- SRFI-171 (transducers) \u2014 standard API over `(std transducer)`\n- SRFI-196 (ranges)\n\nJerboa keeps thin `(std srfi srfi-N)` re-export wrappers for namespace/conflict handling.\n\n### Integrate Remaining chez-* Libraries\n\nMost chez-* C FFI libraries have been superseded by the Rust native backend (see `docs/native-rust.md`). The remaining chez-* libraries that still need integration:\n\n| Library | Integration |\n|---------|------------|\n| **chez-qt** | Add to `CHEZ_EXT_LIBDIRS`, create `(std gui qt)` wrapper |\n| **chez-scintilla** | Already used by jerboa-emacs; expose via `(std gui scintilla)` |\n| **chez-r7rs** | Add to library path for `(scheme base)` etc. |\n\n`chez-ssh` is done \u2014 protocol logic split into `(std net ssh ...)` (10 modules, 3,132 lines), FFI stays in `(chez-ssh crypto)`.\n\n### Complete Rust Native Migration\n\nMove remaining modules from chez-* to Rust native:\n- **TLS**: `(std net ssl)` still uses chez-ssl / OpenSSL \u2014 need rustls-ffi integration\n- **LevelDB**: `(std db leveldb)` still uses chez-leveldb \u2014 need rusty-leveldb or sled\n- **Default imports**: Main modules (e.g., `(std db sqlite)`) still import from chez-* \u2014 need to rewire to `-native` modules\n\n### Web Framework\n\n`(std net httpd)`, `(std net router)`, `(std text json)`, `(std db sqlite)`, `(std db conpool)`, `(std net websocket)`, `(std net rate)`, `(std net security-headers)` all exist. Glue them into `(std web)`:\n\n- Route definitions with parameter extraction\n- JSON request/response helpers\n- Middleware stack\n- Database integration with connection pooling\n- Static file serving\n\n### Database Migrations\n\n`(std db migrate)` \u2014 numbered migration files, up/down, tracking table. Works with both SQLite and PostgreSQL (via Rust native or legacy chez-* backends).\n\n---\n\n## Build & Deployment\n\n### Single-Binary Automation\n\n`jerboa build --static` \u2014 compile all modules \u2192 link with Rust native backend \u2192 one ELF binary. The pieces exist (docs/single-binary.md, jerboa-native-rs/), just needs one-command orchestration.\n\n---\n\n## Tooling\n\n### DAP Debugger\n\nTime-travel debugging, replay, and inspector already exist. A DAP server would connect them to jemacs (which already has LSP client infrastructure as a template) and VS Code.\n\n### Profiler Visualization\n\n`(std misc profile)` provides programmatic profiling. Add Chrome trace format export or SVG flamegraph generation. Could integrate into jemacs as a profiler mode.\n\n### Race Detector\n\nInstrument mutex/hashtable operations, detect happens-before violations, report conflicting access stack traces. Would catch bugs in code using raw `fork-thread` outside structured concurrency.\n\n---\n\n## Claude-as-Developer Improvements\n\n### Better CLAUDE.md for Jerboa\n\nThe current CLAUDE.md is oriented toward Gerbil MCP tools. It should also have:\n- Jerboa-specific patterns and idioms\n- Common R6RS gotchas (definition context, phase separation)\n- Which backend to use (Rust native vs legacy chez-*) for what\n- How jerboa's module namespace maps to Gerbil's\n- Testing conventions\n\n### Example Corpus\n\nMore `examples/` showing real patterns Claude can reference when generating code:\n- Web API with auth and database\n- CLI tool with subcommands\n- Data processing pipeline\n- Actor system with supervision\n- FFI binding to a C library\n- Qt GUI application\n\n---\n\n## Wishlist\n\nThings that would be cool but aren't blocking anything:\n\n- **Windows graceful degradation** \u2014 security modules warn instead of crash on non-Linux\n- **WASM Chez** \u2014 run jerboa in the browser (experimental territory)\n- **jemacs inline eval** \u2014 evaluate expression in buffer, show result inline (REPL-driven development)\n- **jemacs package browser** \u2014 browse available libraries (Rust native + chez-*) from the editor\n"}
diff --git a/training_data.jsonl b/training_data.jsonl
index 075f69b..92614b7 100644
--- a/training_data.jsonl
+++ b/training_data.jsonl
@@ -36262,7 +36262,7 @@
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Tier 4 — Niche, deferred, or non-goal in Jerboa Scheme."}, {"role": "assistant", "content": "| Feature | Section | Status | Reasoning |\n|---------|---------|--------|-----------|\n| **Clojure reader literals (`{}`/`#{}`/`[]`/`:kw`)** | [`clojure-reader.md`](./clojure-reader.md), [§4.9](./clojure-remaining.md#49-reader-literals-----v-kw) | Deferred (risky) | Requires opt-in file-local reader modes; permanent debuggability weirdness; most porters adapt to constructor forms in a day. Could be deferred forever. |\n| **Parked `go` via CPS transform** | [§10](#10-coreasync-csp-channels), §3.8 in `clojure-remaining.md` | Deferred | Research-scale macro project. Jerboa's OS-thread `go` is fine for thousands-of-coroutines use cases, just not millions. |\n| **Clojure Spec / full schema system** | [§17](#17-clojure-spec) | Non-goal | Too large to port faithfully. A scaled-down malli-style schema library is the recommended alternative for anyone who needs it. |\n| **core.logic (miniKanren)** | [§29](#29-corelogic-minikanren) | Open, low priority | A self-contained library port; valuable for anyone doing logic programming, but far from the Clojure-compat critical path. |\n| **Keywords as functions `(: k m)` on pmaps** | [§22](#22-keywordsymbol-semantics) | Deferred | Would require applicable-struct support. Instead use `(get m k)`, which is already polymorphic. |\n| **nREPL-style editor protocol** | [§37](#37-repl-driven-development-culture) | Partial | Jerboa's MCP tooling covers a lot of the same ground (`jerboa_eval`, `jerboa_repl_session`). A proper nREPL server would unlock CIDER/Calva-style editor integration but is a separate project. |\n| **Reader conditionals** | [§20](#20-reader-conditionals) | Deferred | Useful only when Jerboa has multiple targets; currently one target. Revisit if Jerboa-wasm or Gerbil-share becomes real. |\n| **`.clj` source loading** | — | Non-goal | Not porting the JVM compiler. |"}], "source": "doc:docs/clojure-vs-jerboa.md:Tier 4 — Niche, deferred, or non-goal"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Recommendation in Jerboa Scheme."}, {"role": "assistant", "content": "Tier 1 is done. The next natural pass is **Tier 2 items in isolation** — lazy sequences, zippers, and Specter-style paths each stand alone and become more attractive now that the core ergonomics gap is closed. Tier 3's STM is the only remaining \"foundational\" gap and deserves a real design round of its own.\n\nThe language features above are what you'd port. The culture is built alongside, one idiomatic library at a time."}], "source": "doc:docs/clojure-vs-jerboa.md:Recommendation"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Compiling Gerbil Projects with Jerboa in Jerboa Scheme."}, {"role": "assistant", "content": "This guide walks through the complete process of taking an existing Gerbil\nScheme project and compiling it to run on Chez Scheme via Jerboa."}], "source": "doc:docs/compiling-gerbil-projects.md:Compiling Gerbil Projects with Jerboa"}
-{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://github.com/ober/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```"}], "source": "doc:docs/compiling-gerbil-projects.md:Prerequisites"}
+{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://git.sr.ht/~lisp/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```"}], "source": "doc:docs/compiling-gerbil-projects.md:Prerequisites"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Project Layout in Jerboa Scheme."}, {"role": "assistant", "content": "A jerboa project wraps an existing Gerbil project with build scripts and a\ncompatibility layer:\n\n```\nmy-jerboa-project/\n  my-gerbil-project/        # Gerbil source (submodule or copy)\n    gerbil.pkg\n    *.ss                     # Gerbil source files (never modified)\n  src/\n    compat/                  # Chez compatibility shims (handwritten)\n      sugar.sls              # :std/sugar equivalents\n      format.sls             # :std/format equivalents\n      ...\n    mylib/                   # Generated .sls files (gitignored)\n  build-jerboa.ss            # Jerboa translation driver\n  build-all.ss               # Chez compilation trigger\n  build-binary.ss            # Optional: standalone binary builder\n  main.ss                    # Entry point program\n  Makefile\n```"}], "source": "doc:docs/compiling-gerbil-projects.md:Project Layout"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Step 1: Create the Makefile in Jerboa Scheme."}, {"role": "assistant", "content": "```makefile\nSCHEME = scheme\nJERBOA = $(or $(JERBOA_DIR),$(HOME)/src/jerboa/src)\nLIBDIRS = src:$(JERBOA)\nCOMPILE = $(SCHEME) -q --libdirs $(LIBDIRS) --compile-imported-libraries\n\n.PHONY: all jerboa compile binary run clean\n\nall: jerboa compile"}], "source": "doc:docs/compiling-gerbil-projects.md:Step 1: Create the Makefile"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Run interpreted in Jerboa Scheme."}, {"role": "assistant", "content": "run: all\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program main.ss\n\nclean:\n\tfind src -name '*.so' -o -name '*.wpo' | xargs rm -f 2>/dev/null || true\n\trm -f src/mylib/*.sls\n```\n\nThe `JERBOA` variable points to jerboa's `src/` directory. Override it:\n```bash\nJERBOA_DIR=/path/to/jerboa/src make\n```\n\nIf your project depends on other jerboa-compiled libraries, add them to\n`LIBDIRS`:\n```makefile\nJERBOA_AWS = $(or $(JERBOA_AWS_DIR),jerboa-aws/src)\nLIBDIRS = src:$(JERBOA):$(JERBOA_AWS)\n```"}], "source": "doc:docs/compiling-gerbil-projects.md:Run interpreted"}
@@ -36952,7 +36952,7 @@
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Clojure Equivalent in Jerboa Scheme."}, {"role": "assistant", "content": "To build the same webhook processor in Clojure, you need:\n\n```\nproject.clj (or deps.edn)          — dependency management\n├── JVM 17+                        — 200MB runtime\n├── ring/ring-core                 — HTTP abstraction\n├── http-kit or aleph              — async HTTP server\n├── compojure or reitit            — routing\n├── cheshire                       — JSON parsing\n├── core.async                     — channels\n├── mount or component             — lifecycle\n├── redis (Carmine)                — job queue\n├── sidekiq or machinery           — background workers\n├── timbre                         — logging\n└── Docker + docker-compose.yml    — deployment\n```\n\n**Result:**\n- 12+ dependencies (each with transitive deps → 100+ JARs)\n- 3 processes minimum (JVM app + Redis + worker process)\n- 30-second cold start (JVM + dependency loading)\n- 512MB-2GB RAM (JVM heap + Redis)\n- Docker image: 400MB-1GB\n- `docker-compose up` to run locally\n- 5-10 files of boilerplate before writing business logic"}], "source": "doc:docs/jerboa-edge.md:Clojure Equivalent"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Equivalent in Jerboa Scheme."}, {"role": "assistant", "content": "```\nwebhook-service.ss                  — the entire service\n```\n\n**Result:**\n- 0 external dependencies (everything is stdlib)\n- 1 process (one binary does HTTP + workers + state + WebSocket)\n- Instant startup (Chez native code, no interpreter warmup)\n- 20-40MB RAM (no GC pressure from boxed JVM objects)\n- Static binary: 15MB\n- `scp edge user@server: && ssh server ./edge` to deploy\n- ~380 lines, one file, zero config files"}], "source": "doc:docs/jerboa-edge.md:Jerboa Equivalent"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Side-by-Side: Processing a Webhook in Jerboa Scheme."}, {"role": "assistant", "content": "**Clojure:**\n```clojure\n;; deps: ring, compojure, cheshire, core.async, carmine (redis)\n\n(ns webhook.handler\n  (:require [compojure.core :refer [defroutes POST]]\n            [ring.middleware.json :refer [wrap-json-body]]\n            [cheshire.core :as json]\n            [clojure.core.async :as async]\n            [taoensso.carmine :as redis]))\n\n(defroutes app-routes\n  (POST \"/hooks/:type\" [type :as req]\n    (let [payload (:body req)]\n      ;; Push to Redis queue (external process!)\n      (redis/wcar {} (redis/lpush \"webhooks\" (json/encode payload)))\n      {:status 202\n       :body (json/encode {:status \"queued\"})})))\n\n;; Separate worker process reads from Redis:\n(defn worker []\n  (loop []\n    (let [raw (redis/wcar {} (redis/brpop \"webhooks\" 0))\n          event (json/decode (second raw) true)]\n      (process-event! event)\n      (recur))))\n```\n\n**Jerboa:**\n```scheme\n;; deps: none (everything is stdlib)\n\n(import (jerboa prelude)\n        (std clojure)\n        (std net fiber-httpd)\n        (std csp clj)\n        (std text json))\n\n(def work-ch (chan 4096))\n\n(def (handle-webhook req)\n  (let ([payload (string->json-object (request-body req))])\n    (>!! work-ch payload)\n    (respond-json 202 \"{\\\"status\\\":\\\"queued\\\"}\")))\n\n;; Workers are fibers on the same process — no Redis, no IPC\n(dotimes (i 4)\n  (go (let loop ()\n        (let ([event (<!! work-ch)])\n          (when event\n            (process-event! event)\n            (loop))))))\n\n(fiber-httpd-start 8080 handle-webhook)\n```\n\nThe Jerboa version is **12 lines** vs Clojure's **20+ lines + Redis + a separate\nworker process**.  And the Jerboa version handles more concurrent connections\n(fibers vs threads), has built-in fault tolerance (supervisor), and deploys as\none file.\n\n---"}], "source": "doc:docs/jerboa-edge.md:Side-by-Side: Processing a Webhook"}
-{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** [`jerboa-edge/edge.ss`](https://github.com/ober/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://github.com/ober/jerboa-edge\ncd jerboa-edge\nmake run"}], "source": "doc:docs/jerboa-edge.md:Phase 1: Core Demo (the ~380-line file) "}
+{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** [`jerboa-edge/edge.ss`](https://git.sr.ht/~lisp/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa-edge\ncd jerboa-edge\nmake run"}], "source": "doc:docs/jerboa-edge.md:Phase 1: Core Demo (the ~380-line file) "}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain In another terminal: in Jerboa Scheme."}, {"role": "assistant", "content": "curl -X POST localhost:8080/hooks/payment.completed \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"evt_123\",\"amount\":4999}'\n\ncurl localhost:8080/api/stats"}], "source": "doc:docs/jerboa-edge.md:In another terminal:"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain => {\"total\":1,\"ok\":1,\"error\":0} in Jerboa Scheme."}, {"role": "assistant", "content": "```\n\n**Scope:**\n\n- [x] 1.1 **HTTP server with routing** — fiber-httpd with `:param` pattern routing.\n    Five endpoints: `POST /hooks/:type`, `GET /api/events/:id`, `GET /api/stats`,\n    `GET /health`, `GET /dashboard`.\n\n- [x] 1.2 **Channel-based ingestion** — CSP channel with transducer pipeline\n    (validate + dedup-by-ID + normalize).  Decouples accept from process.\n\n- [x] 1.3 **Actor-supervised workers** — N worker actors under a one-for-one\n    supervisor.  Each pulls from the channel, processes, writes to state.\n    `POST /hooks/test.crash` deliberately crashes a worker to show supervisor restart.\n\n- [x] 1.4 **STM state store** — All state in `(make-ref (make-persistent-map))`.\n    Atomic updates via `dosync`/`alter` with `(std pmap)` persistent maps.\n    Immutable snapshots — safe for concurrent reads during writes.\n\n- [x] 1.5 **WebSocket dashboard** — Single WS endpoint that streams\n    event-processed notifications to connected clients.  Sends initial stats\n    on connect.\n\n- [x] 1.6 **HMAC signature verification** — Validate `X-Signature` header using\n    `(std crypto native)` HMAC-SHA256.  Constant-time comparison.\n    Configurable via `EDGE_SECRET` env var.\n\n**Non-goals for Phase 1:**\n- Persistence across restarts (in-memory only)\n- TLS termination (use a reverse proxy, or wait for Phase 3)\n- Static binary packaging (Phase 3)\n\n**Success criteria:**\n- [x] Runs with `make run` (382 lines of Scheme)\n- [x] Handles 10K concurrent webhook POSTs without dropped events (verified: 9,993/10,000 accepted at 200 concurrent connections, ~454 req/sec)\n- [x] Worker crash + supervisor restart visible in logs\n- [x] WebSocket dashboard receives live event stream (verified with websocket-client)\n- [x] `curl` examples work out of the box (17/17 smoke tests pass)\n- [x] HMAC: no-sig and wrong-sig return 401, valid-sig returns 202"}], "source": "doc:docs/jerboa-edge.md:=> {\"total\":1,\"ok\":1,\"error\":0}"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 2: Programmability in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** Sandboxed user filter support + handler registry\n\n- [x] 2.1 **Sandboxed eval** — Users submit filter expressions via\n    `POST /api/filters`.  Filters evaluated per-event via\n    `restricted-eval-string` (allowlist-only: no I/O, no FFI) with a\n    2-second engine-based timeout (no fork — fiber-safe).  Events that\n    fail any filter are dropped silently before processing.\n\n    ```\n    POST /api/filters  {\"name\":\"payments-only\",\"code\":\"(equal? (cdr (assoc \\\"type\\\" evt)) \\\"payment.completed\\\")\"}\n    GET  /api/filters\n    DELETE /api/filters/:name\n    ```\n\n    Key implementation detail: event data is bound as `evt` (a deep\n    alist), so filter code uses `assoc` with string keys (not `assq`).\n\n- [x] 2.2 **Hot handler registration** — New webhook types registered at\n    runtime via `POST /api/handlers`.  No restart required.  Handler\n    code runs in a custom restricted environment (`safe-bindings` +\n    `displayln`) with a 5-second engine timeout.  Exceptions propagate\n    to the worker's retry machinery.\n\n    ```\n    POST /api/handlers  {\"type\":\"order.shipped\",\"code\":\"(displayln \\\"shipped: \\\" (cdr (assoc \\\"id\\\" evt)))\"}\n    GET  /api/handlers\n    ```\n\n- [x] 2.3 **Retry with exponential backoff** — Failed events re-enter the\n    channel with an incremented retry count.  Workers delay via\n    `fork-thread` + `sleep` (non-blocking, OS-thread safe).  Dedup\n    transducer passes retried events (`retry-count > 0`).  After 3\n    retries, events go to a dead-letter STM store.\n\n    ```\n    GET  /api/dead-letter\n    POST /api/dead-letter/:id/replay\n    ```\n\n    Replay sets `retry-count: 1` to bypass the dedup filter.\n\n**Success criteria:**\n- [x] Filter code cannot call `open-file`, `socket`, or any I/O (allowlist env)\n- [x] Filter with 2s timeout verified: `with-timeout` uses Chez engines, no fork\n- [x] Hot-registered handlers process events without service restart (verified: `order.shipped`, `inventory.updated`)\n- [x] Failed events retry 3× with 1s/2s/4s backoff then dead-letter (verified in log)\n- [x] Dead-letter replay re-processes event and clears queue (verified: status → \"ok\")\n- [x] Runs with `make run` (690 lines of Scheme)"}], "source": "doc:docs/jerboa-edge.md:Phase 2: Programmability"}
@@ -37253,7 +37253,7 @@
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Streaming with ports in Jerboa Scheme."}, {"role": "assistant", "content": "```scheme\n(import (std text msgpack))\n\n;; Write multiple values to a port\n(let-values ([(port extract) (open-bytevector-output-port)])\n  (msgpack-pack-port 1 port)\n  (msgpack-pack-port \"hello\" port)\n  (msgpack-pack-port (vector 10 20) port)\n  (let* ([bv (extract)]\n         [in (open-bytevector-input-port bv)])\n    (list\n      (msgpack-unpack-port in)    ;=> 1\n      (msgpack-unpack-port in)    ;=> \"hello\"\n      (msgpack-unpack-port in)))) ;=> #(10 20)\n;=> (1 \"hello\" #(10 20))\n```"}], "source": "doc:docs/protocols.md:Streaming with ports"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Integer encoding compactness in Jerboa Scheme."}, {"role": "assistant", "content": "The encoder automatically picks the smallest representation:\n\n```scheme\n(import (std text msgpack))\n\n;; Positive fixint (1 byte for 0-127)\n(bytevector-length (msgpack-pack 0))    ;=> 1\n(bytevector-length (msgpack-pack 127))  ;=> 1\n\n;; Negative fixint (1 byte for -32 to -1)\n(bytevector-length (msgpack-pack -1))   ;=> 1\n(bytevector-length (msgpack-pack -32))  ;=> 1\n\n;; uint8 (2 bytes for 128-255)\n(bytevector-length (msgpack-pack 200))  ;=> 2\n\n;; uint16 (3 bytes for 256-65535)\n(bytevector-length (msgpack-pack 1000)) ;=> 3\n```"}], "source": "doc:docs/protocols.md:Integer encoding compactness"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Interoperability in Jerboa Scheme."}, {"role": "assistant", "content": "MessagePack is a cross-language format. Bytevectors produced by `msgpack-pack` are compatible with any MessagePack implementation (Python, Go, Rust, JavaScript, etc.) and vice versa.\n\n```scheme\n(import (std text msgpack))\n\n;; Encode a structure for sending over the network\n(define request\n  (msgpack-pack\n    '((\"method\" . \"getUser\")\n      (\"params\" . #(42))\n      (\"id\" . 1))))\n\n;; request is a compact bytevector ready for transmission\n(bytevector-length request)  ; much smaller than equivalent JSON\n```"}], "source": "doc:docs/protocols.md:Interoperability"}
-{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Quickstart in Jerboa Scheme."}, {"role": "assistant", "content": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs"}], "source": "doc:docs/quickstart.md:full"}
+{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Quickstart in Jerboa Scheme."}, {"role": "assistant", "content": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs"}], "source": "doc:docs/quickstart.md:full"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 3. Hello World in Jerboa Scheme."}, {"role": "assistant", "content": "Create `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```"}], "source": "doc:docs/quickstart.md:3. Hello World"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 4. Use the REPL in Jerboa Scheme."}, {"role": "assistant", "content": "```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all."}], "source": "doc:docs/quickstart.md:4. Use the REPL"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 5. Define a Struct in Jerboa Scheme."}, {"role": "assistant", "content": "```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```"}], "source": "doc:docs/quickstart.md:5. Define a Struct"}
@@ -37634,7 +37634,7 @@
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain What Clojure has that Jerboa's pmap is missing in Jerboa Scheme."}, {"role": "assistant", "content": "**1. Transients — the big one.**\nClojure lets you \"thaw\" a persistent map into a transient, do a bulk of\nmutations (`assoc!`, `dissoc!`) with near-mutable cost because only one\nowner is assumed, then freeze it back (`persistent!`). Building a 100k-entry\nmap from an alist is ~4–8× faster with transients than with repeated\n`assoc`. Jerboa's pmap has no transient mechanism — every `imap-set` inside\n`persistent-map-map` / `persistent-map-filter` / `persistent-map-merge`\nallocates a full path copy. See pmap.sls:344-358: `persistent-map-map`\nliterally does `(set! result (persistent-map-set result k (proc k v)))` in\na loop, one allocation-heavy step at a time.\n\n**2. No ArrayNode (dense-node optimization).**\nClojure has *two* interior-node types: `BitmapIndexedNode` for sparse nodes\nand `ArrayNode` for dense ones. When a bitmap-indexed node would reach ~16\nchildren, Clojure promotes it to a 32-slot flat array with `nil` in empty\nslots. Lookups in an ArrayNode skip the popcount indirection — you go\nstraight to `array[hash_chunk]`. Jerboa only has the bitmap variant, so\nevery lookup pays the popcount cost at every level, even in hot dense\nregions of the tree.\n\n**3. Leaves stored inline, not boxed.**\nClojure's `BitmapIndexedNode` stores `[k1, v1, k2, v2, …]` (or\n`[nil, child]` for nested nodes) directly in the node's single array.\nJerboa allocates a separate `hamt-leaf` record for every leaf and stores a\npointer to it in the parent's array (pmap.sls:51-52, used throughout). For\na 1M-entry map that's 1M extra heap objects and one extra pointer\nindirection on every successful lookup.\n\n**4. A hash function picked for HAMT.**\nClojure uses Murmur3 (and `mix-collection-hash`) specifically because HAMT\nperformance is very sensitive to the uniformity of the low bits. Jerboa\npmap uses Chez's stock `equal-hash`, which is a reasonable general-purpose\nhash but wasn't designed for this. In practice it's probably fine; in\npathological distributions it could mean deeper trees.\n\n**5. Structural equality and hashing.**\nA Clojure map *is* hashable by contents, so you can use a map as a key in\nanother map, and `(= m1 m2)` compares contents. Jerboa's `%pmap` is a plain\nrecord — it uses identity equality by default, so you can't use an `imap`\nas a key in an `imap` and `equal?` on two pmaps won't do what you want.\n\n**6. Seq abstraction / lazy iteration.**\nClojure maps participate in the `seq` / `reduce` / `transducer` abstractions\nfor free, including early termination via `reduced`. Jerboa provides\n`persistent-map-for-each`, `->list`, `-fold`, etc., but no lazy iterator —\nno way to stop iteration early without hand-rolling an escape continuation.\n\n**7. The `assoc` bug lives in the collision-bucket path.**\npmap.sls:122:\n\n```scheme\n[(hamt-coll? node)\n (and (= (hamt-coll-hash node) key-hash)\n      (let ([p (assoc key (hamt-coll-pairs node) equal-proc)])  ; BUG\n        p))]\n```\n\nChez's `assoc` takes 2 args, not 3. SRFI-1's `assoc` takes an optional\n3rd `=` argument, but Chez's built-in does not. So on any true hash\ncollision where you try to look up the colliding key, this will raise a\nwrong-number-of-arguments error at runtime. The insert path at\npmap.sls:209 has the same bug. Fix is straightforward:\n\n```scheme\n;; replace (assoc key pairs equal-proc) with a hand-rolled find:\n(let loop ([ps pairs])\n  (cond\n    [(null? ps) #f]\n    [(equal-proc (caar ps) key) (car ps)]\n    [else (loop (cdr ps))]))\n```\n\nThis only bites on true hash collisions, which is why the compile warning\ndoesn't show up as an obvious runtime failure in normal testing. But once\nyou fix it, collision handling becomes correct."}], "source": "doc:docs/thread-safe-hash.md:What Clojure has that Jerboa's pmap is m"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Practical takeaway in Jerboa Scheme."}, {"role": "assistant", "content": "For a \"thread-safe hash-table-ish thing\" with light-to-moderate write rates,\nJerboa's pmap is functionally equivalent to Clojure's — same asymptotic\nbehavior, same thread-safety-by-immutability guarantee. The practical\ndifferences only start to bite at:\n\n- **Bulk construction** from large collections → you'll feel the lack of\n  transients (a 10× hit on `persistent-map-map` / `persistent-map-merge`\n  over millions of entries).\n- **Very large dense maps** → ArrayNode absence adds a constant factor to\n  every lookup, maybe 1.5–2× slower than Clojure at 1M+ entries.\n- **Collision-heavy workloads** → the `assoc` bug at pmap.sls:122 will\n  surface as a runtime crash. Fix it before shipping.\n\nFor the common case (thousands to tens of thousands of entries, moderate\nmutation rate, read-heavy), the gap between Jerboa's pmap and Clojure's PHM\nis not going to be your bottleneck. `imap + shared` remains the right\ndefault for a thread-safe hash-table-like value in Jerboa."}], "source": "doc:docs/thread-safe-hash.md:Practical takeaway"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Tutorial: Build a URL Shortener in Jerboa Scheme."}, {"role": "assistant", "content": "This tutorial walks you from `hello world` to a deployable single-file\nJerboa service. By the end you will have built **Stubby**, a small URL\nshortener with:\n\n- A `defstruct`-modeled core\n- An in-memory cache and a SQLite persistent store\n- An HTTP API on top of `(std net httpd)`\n- A test suite run by `jerboa test`\n- A packaged, redistributable single-file script\n\nEvery code block in this document is a complete runnable or compilable\nJerboa program — the project's CI verifies that.\n\n---"}], "source": "doc:docs/tutorial.md:Jerboa Tutorial: Build a URL Shortener"}
-{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 0. Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---"}], "source": "doc:docs/tutorial.md:0. Prerequisites"}
+{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 0. Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---"}], "source": "doc:docs/tutorial.md:0. Prerequisites"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 1. Hello, world in Jerboa Scheme."}, {"role": "assistant", "content": "Create `stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Stubby is starting up.\")\n```\n\nRun it:\n\n```bash\njerboa run stubby.ss\n```\n\nOne import, one line of code. `(jerboa prelude)` is the entire\nlanguage — no separate core/text/number modules to juggle. Everything\nyou'll use below (structs, hash tables, regex, JSON, format, match)\nlives in the prelude.\n\n---"}], "source": "doc:docs/tutorial.md:1. Hello, world"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 2. Model the domain with `defstruct` in Jerboa Scheme."}, {"role": "assistant", "content": "A short link maps a **slug** (`abc123`) to a **target URL** and tracks\nhow many times it has been resolved.\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target)\n  (make-short-link slug target 0))\n\n(def (bump-hits! link)\n  (short-link-hits-set! link (+ (short-link-hits link) 1)))\n\n(let ([ln (new-link \"abc\" \"https://example.com\")])\n  (bump-hits! ln)\n  (bump-hits! ln)\n  (printf \"slug=~a target=~a hits=~a~n\"\n          (short-link-slug ln)\n          (short-link-target ln)\n          (short-link-hits ln)))\n```\n\n`defstruct` autogenerates `make-short-link`, `short-link?`, one\naccessor per field (`short-link-slug`), and mutators with the\n`-set!` suffix (`short-link-hits-set!`). No more boilerplate.\n\n---"}], "source": "doc:docs/tutorial.md:2. Model the domain with `defstruct`"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 3. Generate slugs in Jerboa Scheme."}, {"role": "assistant", "content": "Real shorteners let users pass a custom slug, but fall back to a\nrandom one. We'll use the prelude's `random` and `string-append`:\n\n```scheme\n(import (jerboa prelude))\n\n(def *alphabet*\n  (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(displayln (random-slug))\n(displayln (random-slug 8))\n```\n\nNotes:\n- The ambiguous characters `0 o l 1` are omitted so slugs stay\n  copy-paste-safe over voice/print.\n- `(random N)` returns an integer in `[0, N)`.\n- `(iota n)` is `0..n-1` — the Jerboa prelude shadows Chez's\n  stricter version to give you the SRFI-1 interface.\n\n---"}], "source": "doc:docs/tutorial.md:3. Generate slugs"}
diff --git a/training_data.v1.jsonl b/training_data.v1.jsonl
index 97ee242..3e5b853 100644
--- a/training_data.v1.jsonl
+++ b/training_data.v1.jsonl
@@ -35956,7 +35956,7 @@
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Tier 4 — Niche, deferred, or non-goal in Jerboa Scheme."}, {"role": "assistant", "content": "| Feature | Section | Status | Reasoning |\n|---------|---------|--------|-----------|\n| **Clojure reader literals (`{}`/`#{}`/`[]`/`:kw`)** | [`clojure-reader.md`](./clojure-reader.md), [§4.9](./clojure-remaining.md#49-reader-literals-----v-kw) | Deferred (risky) | Requires opt-in file-local reader modes; permanent debuggability weirdness; most porters adapt to constructor forms in a day. Could be deferred forever. |\n| **Parked `go` via CPS transform** | [§10](#10-coreasync-csp-channels), §3.8 in `clojure-remaining.md` | Deferred | Research-scale macro project. Jerboa's OS-thread `go` is fine for thousands-of-coroutines use cases, just not millions. |\n| **Clojure Spec / full schema system** | [§17](#17-clojure-spec) | Non-goal | Too large to port faithfully. A scaled-down malli-style schema library is the recommended alternative for anyone who needs it. |\n| **core.logic (miniKanren)** | [§29](#29-corelogic-minikanren) | Open, low priority | A self-contained library port; valuable for anyone doing logic programming, but far from the Clojure-compat critical path. |\n| **Keywords as functions `(: k m)` on pmaps** | [§22](#22-keywordsymbol-semantics) | Deferred | Would require applicable-struct support. Instead use `(get m k)`, which is already polymorphic. |\n| **nREPL-style editor protocol** | [§37](#37-repl-driven-development-culture) | Partial | Jerboa's MCP tooling covers a lot of the same ground (`jerboa_eval`, `jerboa_repl_session`). A proper nREPL server would unlock CIDER/Calva-style editor integration but is a separate project. |\n| **Reader conditionals** | [§20](#20-reader-conditionals) | Deferred | Useful only when Jerboa has multiple targets; currently one target. Revisit if Jerboa-wasm or Gerbil-share becomes real. |\n| **`.clj` source loading** | — | Non-goal | Not porting the JVM compiler. |"}], "source": "doc:docs/clojure-vs-jerboa.md:Tier 4 — Niche, deferred, or non-goal"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Recommendation in Jerboa Scheme."}, {"role": "assistant", "content": "Tier 1 is done. The next natural pass is **Tier 2 items in isolation** — lazy sequences, zippers, and Specter-style paths each stand alone and become more attractive now that the core ergonomics gap is closed. Tier 3's STM is the only remaining \"foundational\" gap and deserves a real design round of its own.\n\nThe language features above are what you'd port. The culture is built alongside, one idiomatic library at a time."}], "source": "doc:docs/clojure-vs-jerboa.md:Recommendation"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Compiling Gerbil Projects with Jerboa in Jerboa Scheme."}, {"role": "assistant", "content": "This guide walks through the complete process of taking an existing Gerbil\nScheme project and compiling it to run on Chez Scheme via Jerboa."}], "source": "doc:docs/compiling-gerbil-projects.md:Compiling Gerbil Projects with Jerboa"}
-{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://github.com/ober/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```"}], "source": "doc:docs/compiling-gerbil-projects.md:Prerequisites"}
+{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://git.sr.ht/~lisp/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```"}], "source": "doc:docs/compiling-gerbil-projects.md:Prerequisites"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Project Layout in Jerboa Scheme."}, {"role": "assistant", "content": "A jerboa project wraps an existing Gerbil project with build scripts and a\ncompatibility layer:\n\n```\nmy-jerboa-project/\n  my-gerbil-project/        # Gerbil source (submodule or copy)\n    gerbil.pkg\n    *.ss                     # Gerbil source files (never modified)\n  src/\n    compat/                  # Chez compatibility shims (handwritten)\n      sugar.sls              # :std/sugar equivalents\n      format.sls             # :std/format equivalents\n      ...\n    mylib/                   # Generated .sls files (gitignored)\n  build-jerboa.ss            # Jerboa translation driver\n  build-all.ss               # Chez compilation trigger\n  build-binary.ss            # Optional: standalone binary builder\n  main.ss                    # Entry point program\n  Makefile\n```"}], "source": "doc:docs/compiling-gerbil-projects.md:Project Layout"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Step 1: Create the Makefile in Jerboa Scheme."}, {"role": "assistant", "content": "```makefile\nSCHEME = scheme\nJERBOA = $(or $(JERBOA_DIR),$(HOME)/src/jerboa/src)\nLIBDIRS = src:$(JERBOA)\nCOMPILE = $(SCHEME) -q --libdirs $(LIBDIRS) --compile-imported-libraries\n\n.PHONY: all jerboa compile binary run clean\n\nall: jerboa compile"}], "source": "doc:docs/compiling-gerbil-projects.md:Step 1: Create the Makefile"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Run interpreted in Jerboa Scheme."}, {"role": "assistant", "content": "run: all\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program main.ss\n\nclean:\n\tfind src -name '*.so' -o -name '*.wpo' | xargs rm -f 2>/dev/null || true\n\trm -f src/mylib/*.sls\n```\n\nThe `JERBOA` variable points to jerboa's `src/` directory. Override it:\n```bash\nJERBOA_DIR=/path/to/jerboa/src make\n```\n\nIf your project depends on other jerboa-compiled libraries, add them to\n`LIBDIRS`:\n```makefile\nJERBOA_AWS = $(or $(JERBOA_AWS_DIR),jerboa-aws/src)\nLIBDIRS = src:$(JERBOA):$(JERBOA_AWS)\n```"}], "source": "doc:docs/compiling-gerbil-projects.md:Run interpreted"}
@@ -36646,7 +36646,7 @@
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Clojure Equivalent in Jerboa Scheme."}, {"role": "assistant", "content": "To build the same webhook processor in Clojure, you need:\n\n```\nproject.clj (or deps.edn)          — dependency management\n├── JVM 17+                        — 200MB runtime\n├── ring/ring-core                 — HTTP abstraction\n├── http-kit or aleph              — async HTTP server\n├── compojure or reitit            — routing\n├── cheshire                       — JSON parsing\n├── core.async                     — channels\n├── mount or component             — lifecycle\n├── redis (Carmine)                — job queue\n├── sidekiq or machinery           — background workers\n├── timbre                         — logging\n└── Docker + docker-compose.yml    — deployment\n```\n\n**Result:**\n- 12+ dependencies (each with transitive deps → 100+ JARs)\n- 3 processes minimum (JVM app + Redis + worker process)\n- 30-second cold start (JVM + dependency loading)\n- 512MB-2GB RAM (JVM heap + Redis)\n- Docker image: 400MB-1GB\n- `docker-compose up` to run locally\n- 5-10 files of boilerplate before writing business logic"}], "source": "doc:docs/jerboa-edge.md:Clojure Equivalent"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Equivalent in Jerboa Scheme."}, {"role": "assistant", "content": "```\nwebhook-service.ss                  — the entire service\n```\n\n**Result:**\n- 0 external dependencies (everything is stdlib)\n- 1 process (one binary does HTTP + workers + state + WebSocket)\n- Instant startup (Chez native code, no interpreter warmup)\n- 20-40MB RAM (no GC pressure from boxed JVM objects)\n- Static binary: 15MB\n- `scp edge user@server: && ssh server ./edge` to deploy\n- ~380 lines, one file, zero config files"}], "source": "doc:docs/jerboa-edge.md:Jerboa Equivalent"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Side-by-Side: Processing a Webhook in Jerboa Scheme."}, {"role": "assistant", "content": "**Clojure:**\n```clojure\n;; deps: ring, compojure, cheshire, core.async, carmine (redis)\n\n(ns webhook.handler\n  (:require [compojure.core :refer [defroutes POST]]\n            [ring.middleware.json :refer [wrap-json-body]]\n            [cheshire.core :as json]\n            [clojure.core.async :as async]\n            [taoensso.carmine :as redis]))\n\n(defroutes app-routes\n  (POST \"/hooks/:type\" [type :as req]\n    (let [payload (:body req)]\n      ;; Push to Redis queue (external process!)\n      (redis/wcar {} (redis/lpush \"webhooks\" (json/encode payload)))\n      {:status 202\n       :body (json/encode {:status \"queued\"})})))\n\n;; Separate worker process reads from Redis:\n(defn worker []\n  (loop []\n    (let [raw (redis/wcar {} (redis/brpop \"webhooks\" 0))\n          event (json/decode (second raw) true)]\n      (process-event! event)\n      (recur))))\n```\n\n**Jerboa:**\n```scheme\n;; deps: none (everything is stdlib)\n\n(import (jerboa prelude)\n        (std clojure)\n        (std net fiber-httpd)\n        (std csp clj)\n        (std text json))\n\n(def work-ch (chan 4096))\n\n(def (handle-webhook req)\n  (let ([payload (string->json-object (request-body req))])\n    (>!! work-ch payload)\n    (respond-json 202 \"{\\\"status\\\":\\\"queued\\\"}\")))\n\n;; Workers are fibers on the same process — no Redis, no IPC\n(dotimes (i 4)\n  (go (let loop ()\n        (let ([event (<!! work-ch)])\n          (when event\n            (process-event! event)\n            (loop))))))\n\n(fiber-httpd-start 8080 handle-webhook)\n```\n\nThe Jerboa version is **12 lines** vs Clojure's **20+ lines + Redis + a separate\nworker process**.  And the Jerboa version handles more concurrent connections\n(fibers vs threads), has built-in fault tolerance (supervisor), and deploys as\none file.\n\n---"}], "source": "doc:docs/jerboa-edge.md:Side-by-Side: Processing a Webhook"}
-{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** [`jerboa-edge/edge.ss`](https://github.com/ober/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://github.com/ober/jerboa-edge\ncd jerboa-edge\nmake run"}], "source": "doc:docs/jerboa-edge.md:Phase 1: Core Demo (the ~380-line file) "}
+{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** [`jerboa-edge/edge.ss`](https://git.sr.ht/~lisp/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa-edge\ncd jerboa-edge\nmake run"}], "source": "doc:docs/jerboa-edge.md:Phase 1: Core Demo (the ~380-line file) "}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain In another terminal: in Jerboa Scheme."}, {"role": "assistant", "content": "curl -X POST localhost:8080/hooks/payment.completed \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"evt_123\",\"amount\":4999}'\n\ncurl localhost:8080/api/stats"}], "source": "doc:docs/jerboa-edge.md:In another terminal:"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain => {\"total\":1,\"ok\":1,\"error\":0} in Jerboa Scheme."}, {"role": "assistant", "content": "```\n\n**Scope:**\n\n- [x] 1.1 **HTTP server with routing** — fiber-httpd with `:param` pattern routing.\n    Five endpoints: `POST /hooks/:type`, `GET /api/events/:id`, `GET /api/stats`,\n    `GET /health`, `GET /dashboard`.\n\n- [x] 1.2 **Channel-based ingestion** — CSP channel with transducer pipeline\n    (validate + dedup-by-ID + normalize).  Decouples accept from process.\n\n- [x] 1.3 **Actor-supervised workers** — N worker actors under a one-for-one\n    supervisor.  Each pulls from the channel, processes, writes to state.\n    `POST /hooks/test.crash` deliberately crashes a worker to show supervisor restart.\n\n- [x] 1.4 **STM state store** — All state in `(make-ref (make-persistent-map))`.\n    Atomic updates via `dosync`/`alter` with `(std pmap)` persistent maps.\n    Immutable snapshots — safe for concurrent reads during writes.\n\n- [x] 1.5 **WebSocket dashboard** — Single WS endpoint that streams\n    event-processed notifications to connected clients.  Sends initial stats\n    on connect.\n\n- [x] 1.6 **HMAC signature verification** — Validate `X-Signature` header using\n    `(std crypto native)` HMAC-SHA256.  Constant-time comparison.\n    Configurable via `EDGE_SECRET` env var.\n\n**Non-goals for Phase 1:**\n- Persistence across restarts (in-memory only)\n- TLS termination (use a reverse proxy, or wait for Phase 3)\n- Static binary packaging (Phase 3)\n\n**Success criteria:**\n- [x] Runs with `make run` (382 lines of Scheme)\n- [x] Handles 10K concurrent webhook POSTs without dropped events (verified: 9,993/10,000 accepted at 200 concurrent connections, ~454 req/sec)\n- [x] Worker crash + supervisor restart visible in logs\n- [x] WebSocket dashboard receives live event stream (verified with websocket-client)\n- [x] `curl` examples work out of the box (17/17 smoke tests pass)\n- [x] HMAC: no-sig and wrong-sig return 401, valid-sig returns 202"}], "source": "doc:docs/jerboa-edge.md:=> {\"total\":1,\"ok\":1,\"error\":0}"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 2: Programmability in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** Sandboxed user filter support + handler registry\n\n- [x] 2.1 **Sandboxed eval** — Users submit filter expressions via\n    `POST /api/filters`.  Filters evaluated per-event via\n    `restricted-eval-string` (allowlist-only: no I/O, no FFI) with a\n    2-second engine-based timeout (no fork — fiber-safe).  Events that\n    fail any filter are dropped silently before processing.\n\n    ```\n    POST /api/filters  {\"name\":\"payments-only\",\"code\":\"(equal? (cdr (assoc \\\"type\\\" evt)) \\\"payment.completed\\\")\"}\n    GET  /api/filters\n    DELETE /api/filters/:name\n    ```\n\n    Key implementation detail: event data is bound as `evt` (a deep\n    alist), so filter code uses `assoc` with string keys (not `assq`).\n\n- [x] 2.2 **Hot handler registration** — New webhook types registered at\n    runtime via `POST /api/handlers`.  No restart required.  Handler\n    code runs in a custom restricted environment (`safe-bindings` +\n    `displayln`) with a 5-second engine timeout.  Exceptions propagate\n    to the worker's retry machinery.\n\n    ```\n    POST /api/handlers  {\"type\":\"order.shipped\",\"code\":\"(displayln \\\"shipped: \\\" (cdr (assoc \\\"id\\\" evt)))\"}\n    GET  /api/handlers\n    ```\n\n- [x] 2.3 **Retry with exponential backoff** — Failed events re-enter the\n    channel with an incremented retry count.  Workers delay via\n    `fork-thread` + `sleep` (non-blocking, OS-thread safe).  Dedup\n    transducer passes retried events (`retry-count > 0`).  After 3\n    retries, events go to a dead-letter STM store.\n\n    ```\n    GET  /api/dead-letter\n    POST /api/dead-letter/:id/replay\n    ```\n\n    Replay sets `retry-count: 1` to bypass the dedup filter.\n\n**Success criteria:**\n- [x] Filter code cannot call `open-file`, `socket`, or any I/O (allowlist env)\n- [x] Filter with 2s timeout verified: `with-timeout` uses Chez engines, no fork\n- [x] Hot-registered handlers process events without service restart (verified: `order.shipped`, `inventory.updated`)\n- [x] Failed events retry 3× with 1s/2s/4s backoff then dead-letter (verified in log)\n- [x] Dead-letter replay re-processes event and clears queue (verified: status → \"ok\")\n- [x] Runs with `make run` (690 lines of Scheme)"}], "source": "doc:docs/jerboa-edge.md:Phase 2: Programmability"}
@@ -36947,7 +36947,7 @@
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Streaming with ports in Jerboa Scheme."}, {"role": "assistant", "content": "```scheme\n(import (std text msgpack))\n\n;; Write multiple values to a port\n(let-values ([(port extract) (open-bytevector-output-port)])\n  (msgpack-pack-port 1 port)\n  (msgpack-pack-port \"hello\" port)\n  (msgpack-pack-port (vector 10 20) port)\n  (let* ([bv (extract)]\n         [in (open-bytevector-input-port bv)])\n    (list\n      (msgpack-unpack-port in)    ;=> 1\n      (msgpack-unpack-port in)    ;=> \"hello\"\n      (msgpack-unpack-port in)))) ;=> #(10 20)\n;=> (1 \"hello\" #(10 20))\n```"}], "source": "doc:docs/protocols.md:Streaming with ports"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Integer encoding compactness in Jerboa Scheme."}, {"role": "assistant", "content": "The encoder automatically picks the smallest representation:\n\n```scheme\n(import (std text msgpack))\n\n;; Positive fixint (1 byte for 0-127)\n(bytevector-length (msgpack-pack 0))    ;=> 1\n(bytevector-length (msgpack-pack 127))  ;=> 1\n\n;; Negative fixint (1 byte for -32 to -1)\n(bytevector-length (msgpack-pack -1))   ;=> 1\n(bytevector-length (msgpack-pack -32))  ;=> 1\n\n;; uint8 (2 bytes for 128-255)\n(bytevector-length (msgpack-pack 200))  ;=> 2\n\n;; uint16 (3 bytes for 256-65535)\n(bytevector-length (msgpack-pack 1000)) ;=> 3\n```"}], "source": "doc:docs/protocols.md:Integer encoding compactness"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Interoperability in Jerboa Scheme."}, {"role": "assistant", "content": "MessagePack is a cross-language format. Bytevectors produced by `msgpack-pack` are compatible with any MessagePack implementation (Python, Go, Rust, JavaScript, etc.) and vice versa.\n\n```scheme\n(import (std text msgpack))\n\n;; Encode a structure for sending over the network\n(define request\n  (msgpack-pack\n    '((\"method\" . \"getUser\")\n      (\"params\" . #(42))\n      (\"id\" . 1))))\n\n;; request is a compact bytevector ready for transmission\n(bytevector-length request)  ; much smaller than equivalent JSON\n```"}], "source": "doc:docs/protocols.md:Interoperability"}
-{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Quickstart in Jerboa Scheme."}, {"role": "assistant", "content": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs"}], "source": "doc:docs/quickstart.md:full"}
+{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Quickstart in Jerboa Scheme."}, {"role": "assistant", "content": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs"}], "source": "doc:docs/quickstart.md:full"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 3. Hello World in Jerboa Scheme."}, {"role": "assistant", "content": "Create `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```"}], "source": "doc:docs/quickstart.md:3. Hello World"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 4. Use the REPL in Jerboa Scheme."}, {"role": "assistant", "content": "```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all."}], "source": "doc:docs/quickstart.md:4. Use the REPL"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 5. Define a Struct in Jerboa Scheme."}, {"role": "assistant", "content": "```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```"}], "source": "doc:docs/quickstart.md:5. Define a Struct"}
@@ -37328,7 +37328,7 @@
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain What Clojure has that Jerboa's pmap is missing in Jerboa Scheme."}, {"role": "assistant", "content": "**1. Transients — the big one.**\nClojure lets you \"thaw\" a persistent map into a transient, do a bulk of\nmutations (`assoc!`, `dissoc!`) with near-mutable cost because only one\nowner is assumed, then freeze it back (`persistent!`). Building a 100k-entry\nmap from an alist is ~4–8× faster with transients than with repeated\n`assoc`. Jerboa's pmap has no transient mechanism — every `imap-set` inside\n`persistent-map-map` / `persistent-map-filter` / `persistent-map-merge`\nallocates a full path copy. See pmap.sls:344-358: `persistent-map-map`\nliterally does `(set! result (persistent-map-set result k (proc k v)))` in\na loop, one allocation-heavy step at a time.\n\n**2. No ArrayNode (dense-node optimization).**\nClojure has *two* interior-node types: `BitmapIndexedNode` for sparse nodes\nand `ArrayNode` for dense ones. When a bitmap-indexed node would reach ~16\nchildren, Clojure promotes it to a 32-slot flat array with `nil` in empty\nslots. Lookups in an ArrayNode skip the popcount indirection — you go\nstraight to `array[hash_chunk]`. Jerboa only has the bitmap variant, so\nevery lookup pays the popcount cost at every level, even in hot dense\nregions of the tree.\n\n**3. Leaves stored inline, not boxed.**\nClojure's `BitmapIndexedNode` stores `[k1, v1, k2, v2, …]` (or\n`[nil, child]` for nested nodes) directly in the node's single array.\nJerboa allocates a separate `hamt-leaf` record for every leaf and stores a\npointer to it in the parent's array (pmap.sls:51-52, used throughout). For\na 1M-entry map that's 1M extra heap objects and one extra pointer\nindirection on every successful lookup.\n\n**4. A hash function picked for HAMT.**\nClojure uses Murmur3 (and `mix-collection-hash`) specifically because HAMT\nperformance is very sensitive to the uniformity of the low bits. Jerboa\npmap uses Chez's stock `equal-hash`, which is a reasonable general-purpose\nhash but wasn't designed for this. In practice it's probably fine; in\npathological distributions it could mean deeper trees.\n\n**5. Structural equality and hashing.**\nA Clojure map *is* hashable by contents, so you can use a map as a key in\nanother map, and `(= m1 m2)` compares contents. Jerboa's `%pmap` is a plain\nrecord — it uses identity equality by default, so you can't use an `imap`\nas a key in an `imap` and `equal?` on two pmaps won't do what you want.\n\n**6. Seq abstraction / lazy iteration.**\nClojure maps participate in the `seq` / `reduce` / `transducer` abstractions\nfor free, including early termination via `reduced`. Jerboa provides\n`persistent-map-for-each`, `->list`, `-fold`, etc., but no lazy iterator —\nno way to stop iteration early without hand-rolling an escape continuation.\n\n**7. The `assoc` bug lives in the collision-bucket path.**\npmap.sls:122:\n\n```scheme\n[(hamt-coll? node)\n (and (= (hamt-coll-hash node) key-hash)\n      (let ([p (assoc key (hamt-coll-pairs node) equal-proc)])  ; BUG\n        p))]\n```\n\nChez's `assoc` takes 2 args, not 3. SRFI-1's `assoc` takes an optional\n3rd `=` argument, but Chez's built-in does not. So on any true hash\ncollision where you try to look up the colliding key, this will raise a\nwrong-number-of-arguments error at runtime. The insert path at\npmap.sls:209 has the same bug. Fix is straightforward:\n\n```scheme\n;; replace (assoc key pairs equal-proc) with a hand-rolled find:\n(let loop ([ps pairs])\n  (cond\n    [(null? ps) #f]\n    [(equal-proc (caar ps) key) (car ps)]\n    [else (loop (cdr ps))]))\n```\n\nThis only bites on true hash collisions, which is why the compile warning\ndoesn't show up as an obvious runtime failure in normal testing. But once\nyou fix it, collision handling becomes correct."}], "source": "doc:docs/thread-safe-hash.md:What Clojure has that Jerboa's pmap is m"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Practical takeaway in Jerboa Scheme."}, {"role": "assistant", "content": "For a \"thread-safe hash-table-ish thing\" with light-to-moderate write rates,\nJerboa's pmap is functionally equivalent to Clojure's — same asymptotic\nbehavior, same thread-safety-by-immutability guarantee. The practical\ndifferences only start to bite at:\n\n- **Bulk construction** from large collections → you'll feel the lack of\n  transients (a 10× hit on `persistent-map-map` / `persistent-map-merge`\n  over millions of entries).\n- **Very large dense maps** → ArrayNode absence adds a constant factor to\n  every lookup, maybe 1.5–2× slower than Clojure at 1M+ entries.\n- **Collision-heavy workloads** → the `assoc` bug at pmap.sls:122 will\n  surface as a runtime crash. Fix it before shipping.\n\nFor the common case (thousands to tens of thousands of entries, moderate\nmutation rate, read-heavy), the gap between Jerboa's pmap and Clojure's PHM\nis not going to be your bottleneck. `imap + shared` remains the right\ndefault for a thread-safe hash-table-like value in Jerboa."}], "source": "doc:docs/thread-safe-hash.md:Practical takeaway"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Tutorial: Build a URL Shortener in Jerboa Scheme."}, {"role": "assistant", "content": "This tutorial walks you from `hello world` to a deployable single-file\nJerboa service. By the end you will have built **Stubby**, a small URL\nshortener with:\n\n- A `defstruct`-modeled core\n- An in-memory cache and a SQLite persistent store\n- An HTTP API on top of `(std net httpd)`\n- A test suite run by `jerboa test`\n- A packaged, redistributable single-file script\n\nEvery code block in this document is a complete runnable or compilable\nJerboa program — the project's CI verifies that.\n\n---"}], "source": "doc:docs/tutorial.md:Jerboa Tutorial: Build a URL Shortener"}
-{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 0. Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---"}], "source": "doc:docs/tutorial.md:0. Prerequisites"}
+{"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 0. Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---"}], "source": "doc:docs/tutorial.md:0. Prerequisites"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 1. Hello, world in Jerboa Scheme."}, {"role": "assistant", "content": "Create `stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Stubby is starting up.\")\n```\n\nRun it:\n\n```bash\njerboa run stubby.ss\n```\n\nOne import, one line of code. `(jerboa prelude)` is the entire\nlanguage — no separate core/text/number modules to juggle. Everything\nyou'll use below (structs, hash tables, regex, JSON, format, match)\nlives in the prelude.\n\n---"}], "source": "doc:docs/tutorial.md:1. Hello, world"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 2. Model the domain with `defstruct` in Jerboa Scheme."}, {"role": "assistant", "content": "A short link maps a **slug** (`abc123`) to a **target URL** and tracks\nhow many times it has been resolved.\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target)\n  (make-short-link slug target 0))\n\n(def (bump-hits! link)\n  (short-link-hits-set! link (+ (short-link-hits link) 1)))\n\n(let ([ln (new-link \"abc\" \"https://example.com\")])\n  (bump-hits! ln)\n  (bump-hits! ln)\n  (printf \"slug=~a target=~a hits=~a~n\"\n          (short-link-slug ln)\n          (short-link-target ln)\n          (short-link-hits ln)))\n```\n\n`defstruct` autogenerates `make-short-link`, `short-link?`, one\naccessor per field (`short-link-slug`), and mutators with the\n`-set!` suffix (`short-link-hits-set!`). No more boilerplate.\n\n---"}], "source": "doc:docs/tutorial.md:2. Model the domain with `defstruct`"}
 {"conversations": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 3. Generate slugs in Jerboa Scheme."}, {"role": "assistant", "content": "Real shorteners let users pass a custom slug, but fall back to a\nrandom one. We'll use the prelude's `random` and `string-append`:\n\n```scheme\n(import (jerboa prelude))\n\n(def *alphabet*\n  (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(displayln (random-slug))\n(displayln (random-slug 8))\n```\n\nNotes:\n- The ambiguous characters `0 o l 1` are omitted so slugs stay\n  copy-paste-safe over voice/print.\n- `(random N)` returns an integer in `[0, N)`.\n- `(iota n)` is `0..n-1` — the Jerboa prelude shadows Chez's\n  stricter version to give you the SRFI-1 interface.\n\n---"}], "source": "doc:docs/tutorial.md:3. Generate slugs"}
diff --git a/training_data_alpaca.json b/training_data_alpaca.json
index 48ba902..de7737f 100644
--- a/training_data_alpaca.json
+++ b/training_data_alpaca.json
@@ -217586,7 +217586,7 @@
   {
     "instruction": "Explain Prerequisites in Jerboa Scheme.",
     "input": "",
-    "output": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://github.com/ober/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```",
+    "output": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://git.sr.ht/~lisp/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```",
     "source": "doc:docs/compiling-gerbil-projects.md:Prerequisites"
   },
   {
@@ -221726,7 +221726,7 @@
   {
     "instruction": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme.",
     "input": "",
-    "output": "**Deliverable:** [`jerboa-edge/edge.ss`](https://github.com/ober/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://github.com/ober/jerboa-edge\ncd jerboa-edge\nmake run",
+    "output": "**Deliverable:** [`jerboa-edge/edge.ss`](https://git.sr.ht/~lisp/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa-edge\ncd jerboa-edge\nmake run",
     "source": "doc:docs/jerboa-edge.md:Phase 1: Core Demo (the ~380-line file) "
   },
   {
@@ -223532,7 +223532,7 @@
   {
     "instruction": "Explain Jerboa Quickstart in Jerboa Scheme.",
     "input": "",
-    "output": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs",
+    "output": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs",
     "source": "doc:docs/quickstart.md:full"
   },
   {
@@ -225818,7 +225818,7 @@
   {
     "instruction": "Explain 0. Prerequisites in Jerboa Scheme.",
     "input": "",
-    "output": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---",
+    "output": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---",
     "source": "doc:docs/tutorial.md:0. Prerequisites"
   },
   {
diff --git a/training_data_alpaca.jsonl b/training_data_alpaca.jsonl
index fe8b33e..bba7073 100644
--- a/training_data_alpaca.jsonl
+++ b/training_data_alpaca.jsonl
@@ -36262,7 +36262,7 @@
 {"instruction": "Explain Tier 4 — Niche, deferred, or non-goal in Jerboa Scheme.", "input": "", "output": "| Feature | Section | Status | Reasoning |\n|---------|---------|--------|-----------|\n| **Clojure reader literals (`{}`/`#{}`/`[]`/`:kw`)** | [`clojure-reader.md`](./clojure-reader.md), [§4.9](./clojure-remaining.md#49-reader-literals-----v-kw) | Deferred (risky) | Requires opt-in file-local reader modes; permanent debuggability weirdness; most porters adapt to constructor forms in a day. Could be deferred forever. |\n| **Parked `go` via CPS transform** | [§10](#10-coreasync-csp-channels), §3.8 in `clojure-remaining.md` | Deferred | Research-scale macro project. Jerboa's OS-thread `go` is fine for thousands-of-coroutines use cases, just not millions. |\n| **Clojure Spec / full schema system** | [§17](#17-clojure-spec) | Non-goal | Too large to port faithfully. A scaled-down malli-style schema library is the recommended alternative for anyone who needs it. |\n| **core.logic (miniKanren)** | [§29](#29-corelogic-minikanren) | Open, low priority | A self-contained library port; valuable for anyone doing logic programming, but far from the Clojure-compat critical path. |\n| **Keywords as functions `(: k m)` on pmaps** | [§22](#22-keywordsymbol-semantics) | Deferred | Would require applicable-struct support. Instead use `(get m k)`, which is already polymorphic. |\n| **nREPL-style editor protocol** | [§37](#37-repl-driven-development-culture) | Partial | Jerboa's MCP tooling covers a lot of the same ground (`jerboa_eval`, `jerboa_repl_session`). A proper nREPL server would unlock CIDER/Calva-style editor integration but is a separate project. |\n| **Reader conditionals** | [§20](#20-reader-conditionals) | Deferred | Useful only when Jerboa has multiple targets; currently one target. Revisit if Jerboa-wasm or Gerbil-share becomes real. |\n| **`.clj` source loading** | — | Non-goal | Not porting the JVM compiler. |", "source": "doc:docs/clojure-vs-jerboa.md:Tier 4 — Niche, deferred, or non-goal"}
 {"instruction": "Explain Recommendation in Jerboa Scheme.", "input": "", "output": "Tier 1 is done. The next natural pass is **Tier 2 items in isolation** — lazy sequences, zippers, and Specter-style paths each stand alone and become more attractive now that the core ergonomics gap is closed. Tier 3's STM is the only remaining \"foundational\" gap and deserves a real design round of its own.\n\nThe language features above are what you'd port. The culture is built alongside, one idiomatic library at a time.", "source": "doc:docs/clojure-vs-jerboa.md:Recommendation"}
 {"instruction": "Explain Compiling Gerbil Projects with Jerboa in Jerboa Scheme.", "input": "", "output": "This guide walks through the complete process of taking an existing Gerbil\nScheme project and compiling it to run on Chez Scheme via Jerboa.", "source": "doc:docs/compiling-gerbil-projects.md:Compiling Gerbil Projects with Jerboa"}
-{"instruction": "Explain Prerequisites in Jerboa Scheme.", "input": "", "output": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://github.com/ober/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```", "source": "doc:docs/compiling-gerbil-projects.md:Prerequisites"}
+{"instruction": "Explain Prerequisites in Jerboa Scheme.", "input": "", "output": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://git.sr.ht/~lisp/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```", "source": "doc:docs/compiling-gerbil-projects.md:Prerequisites"}
 {"instruction": "Explain Project Layout in Jerboa Scheme.", "input": "", "output": "A jerboa project wraps an existing Gerbil project with build scripts and a\ncompatibility layer:\n\n```\nmy-jerboa-project/\n  my-gerbil-project/        # Gerbil source (submodule or copy)\n    gerbil.pkg\n    *.ss                     # Gerbil source files (never modified)\n  src/\n    compat/                  # Chez compatibility shims (handwritten)\n      sugar.sls              # :std/sugar equivalents\n      format.sls             # :std/format equivalents\n      ...\n    mylib/                   # Generated .sls files (gitignored)\n  build-jerboa.ss            # Jerboa translation driver\n  build-all.ss               # Chez compilation trigger\n  build-binary.ss            # Optional: standalone binary builder\n  main.ss                    # Entry point program\n  Makefile\n```", "source": "doc:docs/compiling-gerbil-projects.md:Project Layout"}
 {"instruction": "Explain Step 1: Create the Makefile in Jerboa Scheme.", "input": "", "output": "```makefile\nSCHEME = scheme\nJERBOA = $(or $(JERBOA_DIR),$(HOME)/src/jerboa/src)\nLIBDIRS = src:$(JERBOA)\nCOMPILE = $(SCHEME) -q --libdirs $(LIBDIRS) --compile-imported-libraries\n\n.PHONY: all jerboa compile binary run clean\n\nall: jerboa compile", "source": "doc:docs/compiling-gerbil-projects.md:Step 1: Create the Makefile"}
 {"instruction": "Explain Run interpreted in Jerboa Scheme.", "input": "", "output": "run: all\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program main.ss\n\nclean:\n\tfind src -name '*.so' -o -name '*.wpo' | xargs rm -f 2>/dev/null || true\n\trm -f src/mylib/*.sls\n```\n\nThe `JERBOA` variable points to jerboa's `src/` directory. Override it:\n```bash\nJERBOA_DIR=/path/to/jerboa/src make\n```\n\nIf your project depends on other jerboa-compiled libraries, add them to\n`LIBDIRS`:\n```makefile\nJERBOA_AWS = $(or $(JERBOA_AWS_DIR),jerboa-aws/src)\nLIBDIRS = src:$(JERBOA):$(JERBOA_AWS)\n```", "source": "doc:docs/compiling-gerbil-projects.md:Run interpreted"}
@@ -36952,7 +36952,7 @@
 {"instruction": "Explain Clojure Equivalent in Jerboa Scheme.", "input": "", "output": "To build the same webhook processor in Clojure, you need:\n\n```\nproject.clj (or deps.edn)          — dependency management\n├── JVM 17+                        — 200MB runtime\n├── ring/ring-core                 — HTTP abstraction\n├── http-kit or aleph              — async HTTP server\n├── compojure or reitit            — routing\n├── cheshire                       — JSON parsing\n├── core.async                     — channels\n├── mount or component             — lifecycle\n├── redis (Carmine)                — job queue\n├── sidekiq or machinery           — background workers\n├── timbre                         — logging\n└── Docker + docker-compose.yml    — deployment\n```\n\n**Result:**\n- 12+ dependencies (each with transitive deps → 100+ JARs)\n- 3 processes minimum (JVM app + Redis + worker process)\n- 30-second cold start (JVM + dependency loading)\n- 512MB-2GB RAM (JVM heap + Redis)\n- Docker image: 400MB-1GB\n- `docker-compose up` to run locally\n- 5-10 files of boilerplate before writing business logic", "source": "doc:docs/jerboa-edge.md:Clojure Equivalent"}
 {"instruction": "Explain Jerboa Equivalent in Jerboa Scheme.", "input": "", "output": "```\nwebhook-service.ss                  — the entire service\n```\n\n**Result:**\n- 0 external dependencies (everything is stdlib)\n- 1 process (one binary does HTTP + workers + state + WebSocket)\n- Instant startup (Chez native code, no interpreter warmup)\n- 20-40MB RAM (no GC pressure from boxed JVM objects)\n- Static binary: 15MB\n- `scp edge user@server: && ssh server ./edge` to deploy\n- ~380 lines, one file, zero config files", "source": "doc:docs/jerboa-edge.md:Jerboa Equivalent"}
 {"instruction": "Explain Side-by-Side: Processing a Webhook in Jerboa Scheme.", "input": "", "output": "**Clojure:**\n```clojure\n;; deps: ring, compojure, cheshire, core.async, carmine (redis)\n\n(ns webhook.handler\n  (:require [compojure.core :refer [defroutes POST]]\n            [ring.middleware.json :refer [wrap-json-body]]\n            [cheshire.core :as json]\n            [clojure.core.async :as async]\n            [taoensso.carmine :as redis]))\n\n(defroutes app-routes\n  (POST \"/hooks/:type\" [type :as req]\n    (let [payload (:body req)]\n      ;; Push to Redis queue (external process!)\n      (redis/wcar {} (redis/lpush \"webhooks\" (json/encode payload)))\n      {:status 202\n       :body (json/encode {:status \"queued\"})})))\n\n;; Separate worker process reads from Redis:\n(defn worker []\n  (loop []\n    (let [raw (redis/wcar {} (redis/brpop \"webhooks\" 0))\n          event (json/decode (second raw) true)]\n      (process-event! event)\n      (recur))))\n```\n\n**Jerboa:**\n```scheme\n;; deps: none (everything is stdlib)\n\n(import (jerboa prelude)\n        (std clojure)\n        (std net fiber-httpd)\n        (std csp clj)\n        (std text json))\n\n(def work-ch (chan 4096))\n\n(def (handle-webhook req)\n  (let ([payload (string->json-object (request-body req))])\n    (>!! work-ch payload)\n    (respond-json 202 \"{\\\"status\\\":\\\"queued\\\"}\")))\n\n;; Workers are fibers on the same process — no Redis, no IPC\n(dotimes (i 4)\n  (go (let loop ()\n        (let ([event (<!! work-ch)])\n          (when event\n            (process-event! event)\n            (loop))))))\n\n(fiber-httpd-start 8080 handle-webhook)\n```\n\nThe Jerboa version is **12 lines** vs Clojure's **20+ lines + Redis + a separate\nworker process**.  And the Jerboa version handles more concurrent connections\n(fibers vs threads), has built-in fault tolerance (supervisor), and deploys as\none file.\n\n---", "source": "doc:docs/jerboa-edge.md:Side-by-Side: Processing a Webhook"}
-{"instruction": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme.", "input": "", "output": "**Deliverable:** [`jerboa-edge/edge.ss`](https://github.com/ober/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://github.com/ober/jerboa-edge\ncd jerboa-edge\nmake run", "source": "doc:docs/jerboa-edge.md:Phase 1: Core Demo (the ~380-line file) "}
+{"instruction": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme.", "input": "", "output": "**Deliverable:** [`jerboa-edge/edge.ss`](https://git.sr.ht/~lisp/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa-edge\ncd jerboa-edge\nmake run", "source": "doc:docs/jerboa-edge.md:Phase 1: Core Demo (the ~380-line file) "}
 {"instruction": "Explain In another terminal: in Jerboa Scheme.", "input": "", "output": "curl -X POST localhost:8080/hooks/payment.completed \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"evt_123\",\"amount\":4999}'\n\ncurl localhost:8080/api/stats", "source": "doc:docs/jerboa-edge.md:In another terminal:"}
 {"instruction": "Explain => {\"total\":1,\"ok\":1,\"error\":0} in Jerboa Scheme.", "input": "", "output": "```\n\n**Scope:**\n\n- [x] 1.1 **HTTP server with routing** — fiber-httpd with `:param` pattern routing.\n    Five endpoints: `POST /hooks/:type`, `GET /api/events/:id`, `GET /api/stats`,\n    `GET /health`, `GET /dashboard`.\n\n- [x] 1.2 **Channel-based ingestion** — CSP channel with transducer pipeline\n    (validate + dedup-by-ID + normalize).  Decouples accept from process.\n\n- [x] 1.3 **Actor-supervised workers** — N worker actors under a one-for-one\n    supervisor.  Each pulls from the channel, processes, writes to state.\n    `POST /hooks/test.crash` deliberately crashes a worker to show supervisor restart.\n\n- [x] 1.4 **STM state store** — All state in `(make-ref (make-persistent-map))`.\n    Atomic updates via `dosync`/`alter` with `(std pmap)` persistent maps.\n    Immutable snapshots — safe for concurrent reads during writes.\n\n- [x] 1.5 **WebSocket dashboard** — Single WS endpoint that streams\n    event-processed notifications to connected clients.  Sends initial stats\n    on connect.\n\n- [x] 1.6 **HMAC signature verification** — Validate `X-Signature` header using\n    `(std crypto native)` HMAC-SHA256.  Constant-time comparison.\n    Configurable via `EDGE_SECRET` env var.\n\n**Non-goals for Phase 1:**\n- Persistence across restarts (in-memory only)\n- TLS termination (use a reverse proxy, or wait for Phase 3)\n- Static binary packaging (Phase 3)\n\n**Success criteria:**\n- [x] Runs with `make run` (382 lines of Scheme)\n- [x] Handles 10K concurrent webhook POSTs without dropped events (verified: 9,993/10,000 accepted at 200 concurrent connections, ~454 req/sec)\n- [x] Worker crash + supervisor restart visible in logs\n- [x] WebSocket dashboard receives live event stream (verified with websocket-client)\n- [x] `curl` examples work out of the box (17/17 smoke tests pass)\n- [x] HMAC: no-sig and wrong-sig return 401, valid-sig returns 202", "source": "doc:docs/jerboa-edge.md:=> {\"total\":1,\"ok\":1,\"error\":0}"}
 {"instruction": "Explain Phase 2: Programmability in Jerboa Scheme.", "input": "", "output": "**Deliverable:** Sandboxed user filter support + handler registry\n\n- [x] 2.1 **Sandboxed eval** — Users submit filter expressions via\n    `POST /api/filters`.  Filters evaluated per-event via\n    `restricted-eval-string` (allowlist-only: no I/O, no FFI) with a\n    2-second engine-based timeout (no fork — fiber-safe).  Events that\n    fail any filter are dropped silently before processing.\n\n    ```\n    POST /api/filters  {\"name\":\"payments-only\",\"code\":\"(equal? (cdr (assoc \\\"type\\\" evt)) \\\"payment.completed\\\")\"}\n    GET  /api/filters\n    DELETE /api/filters/:name\n    ```\n\n    Key implementation detail: event data is bound as `evt` (a deep\n    alist), so filter code uses `assoc` with string keys (not `assq`).\n\n- [x] 2.2 **Hot handler registration** — New webhook types registered at\n    runtime via `POST /api/handlers`.  No restart required.  Handler\n    code runs in a custom restricted environment (`safe-bindings` +\n    `displayln`) with a 5-second engine timeout.  Exceptions propagate\n    to the worker's retry machinery.\n\n    ```\n    POST /api/handlers  {\"type\":\"order.shipped\",\"code\":\"(displayln \\\"shipped: \\\" (cdr (assoc \\\"id\\\" evt)))\"}\n    GET  /api/handlers\n    ```\n\n- [x] 2.3 **Retry with exponential backoff** — Failed events re-enter the\n    channel with an incremented retry count.  Workers delay via\n    `fork-thread` + `sleep` (non-blocking, OS-thread safe).  Dedup\n    transducer passes retried events (`retry-count > 0`).  After 3\n    retries, events go to a dead-letter STM store.\n\n    ```\n    GET  /api/dead-letter\n    POST /api/dead-letter/:id/replay\n    ```\n\n    Replay sets `retry-count: 1` to bypass the dedup filter.\n\n**Success criteria:**\n- [x] Filter code cannot call `open-file`, `socket`, or any I/O (allowlist env)\n- [x] Filter with 2s timeout verified: `with-timeout` uses Chez engines, no fork\n- [x] Hot-registered handlers process events without service restart (verified: `order.shipped`, `inventory.updated`)\n- [x] Failed events retry 3× with 1s/2s/4s backoff then dead-letter (verified in log)\n- [x] Dead-letter replay re-processes event and clears queue (verified: status → \"ok\")\n- [x] Runs with `make run` (690 lines of Scheme)", "source": "doc:docs/jerboa-edge.md:Phase 2: Programmability"}
@@ -37253,7 +37253,7 @@
 {"instruction": "Explain Streaming with ports in Jerboa Scheme.", "input": "", "output": "```scheme\n(import (std text msgpack))\n\n;; Write multiple values to a port\n(let-values ([(port extract) (open-bytevector-output-port)])\n  (msgpack-pack-port 1 port)\n  (msgpack-pack-port \"hello\" port)\n  (msgpack-pack-port (vector 10 20) port)\n  (let* ([bv (extract)]\n         [in (open-bytevector-input-port bv)])\n    (list\n      (msgpack-unpack-port in)    ;=> 1\n      (msgpack-unpack-port in)    ;=> \"hello\"\n      (msgpack-unpack-port in)))) ;=> #(10 20)\n;=> (1 \"hello\" #(10 20))\n```", "source": "doc:docs/protocols.md:Streaming with ports"}
 {"instruction": "Explain Integer encoding compactness in Jerboa Scheme.", "input": "", "output": "The encoder automatically picks the smallest representation:\n\n```scheme\n(import (std text msgpack))\n\n;; Positive fixint (1 byte for 0-127)\n(bytevector-length (msgpack-pack 0))    ;=> 1\n(bytevector-length (msgpack-pack 127))  ;=> 1\n\n;; Negative fixint (1 byte for -32 to -1)\n(bytevector-length (msgpack-pack -1))   ;=> 1\n(bytevector-length (msgpack-pack -32))  ;=> 1\n\n;; uint8 (2 bytes for 128-255)\n(bytevector-length (msgpack-pack 200))  ;=> 2\n\n;; uint16 (3 bytes for 256-65535)\n(bytevector-length (msgpack-pack 1000)) ;=> 3\n```", "source": "doc:docs/protocols.md:Integer encoding compactness"}
 {"instruction": "Explain Interoperability in Jerboa Scheme.", "input": "", "output": "MessagePack is a cross-language format. Bytevectors produced by `msgpack-pack` are compatible with any MessagePack implementation (Python, Go, Rust, JavaScript, etc.) and vice versa.\n\n```scheme\n(import (std text msgpack))\n\n;; Encode a structure for sending over the network\n(define request\n  (msgpack-pack\n    '((\"method\" . \"getUser\")\n      (\"params\" . #(42))\n      (\"id\" . 1))))\n\n;; request is a compact bytevector ready for transmission\n(bytevector-length request)  ; much smaller than equivalent JSON\n```", "source": "doc:docs/protocols.md:Interoperability"}
-{"instruction": "Explain Jerboa Quickstart in Jerboa Scheme.", "input": "", "output": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs", "source": "doc:docs/quickstart.md:full"}
+{"instruction": "Explain Jerboa Quickstart in Jerboa Scheme.", "input": "", "output": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs", "source": "doc:docs/quickstart.md:full"}
 {"instruction": "Explain 3. Hello World in Jerboa Scheme.", "input": "", "output": "Create `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```", "source": "doc:docs/quickstart.md:3. Hello World"}
 {"instruction": "Explain 4. Use the REPL in Jerboa Scheme.", "input": "", "output": "```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.", "source": "doc:docs/quickstart.md:4. Use the REPL"}
 {"instruction": "Explain 5. Define a Struct in Jerboa Scheme.", "input": "", "output": "```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```", "source": "doc:docs/quickstart.md:5. Define a Struct"}
@@ -37634,7 +37634,7 @@
 {"instruction": "Explain What Clojure has that Jerboa's pmap is missing in Jerboa Scheme.", "input": "", "output": "**1. Transients — the big one.**\nClojure lets you \"thaw\" a persistent map into a transient, do a bulk of\nmutations (`assoc!`, `dissoc!`) with near-mutable cost because only one\nowner is assumed, then freeze it back (`persistent!`). Building a 100k-entry\nmap from an alist is ~4–8× faster with transients than with repeated\n`assoc`. Jerboa's pmap has no transient mechanism — every `imap-set` inside\n`persistent-map-map` / `persistent-map-filter` / `persistent-map-merge`\nallocates a full path copy. See pmap.sls:344-358: `persistent-map-map`\nliterally does `(set! result (persistent-map-set result k (proc k v)))` in\na loop, one allocation-heavy step at a time.\n\n**2. No ArrayNode (dense-node optimization).**\nClojure has *two* interior-node types: `BitmapIndexedNode` for sparse nodes\nand `ArrayNode` for dense ones. When a bitmap-indexed node would reach ~16\nchildren, Clojure promotes it to a 32-slot flat array with `nil` in empty\nslots. Lookups in an ArrayNode skip the popcount indirection — you go\nstraight to `array[hash_chunk]`. Jerboa only has the bitmap variant, so\nevery lookup pays the popcount cost at every level, even in hot dense\nregions of the tree.\n\n**3. Leaves stored inline, not boxed.**\nClojure's `BitmapIndexedNode` stores `[k1, v1, k2, v2, …]` (or\n`[nil, child]` for nested nodes) directly in the node's single array.\nJerboa allocates a separate `hamt-leaf` record for every leaf and stores a\npointer to it in the parent's array (pmap.sls:51-52, used throughout). For\na 1M-entry map that's 1M extra heap objects and one extra pointer\nindirection on every successful lookup.\n\n**4. A hash function picked for HAMT.**\nClojure uses Murmur3 (and `mix-collection-hash`) specifically because HAMT\nperformance is very sensitive to the uniformity of the low bits. Jerboa\npmap uses Chez's stock `equal-hash`, which is a reasonable general-purpose\nhash but wasn't designed for this. In practice it's probably fine; in\npathological distributions it could mean deeper trees.\n\n**5. Structural equality and hashing.**\nA Clojure map *is* hashable by contents, so you can use a map as a key in\nanother map, and `(= m1 m2)` compares contents. Jerboa's `%pmap` is a plain\nrecord — it uses identity equality by default, so you can't use an `imap`\nas a key in an `imap` and `equal?` on two pmaps won't do what you want.\n\n**6. Seq abstraction / lazy iteration.**\nClojure maps participate in the `seq` / `reduce` / `transducer` abstractions\nfor free, including early termination via `reduced`. Jerboa provides\n`persistent-map-for-each`, `->list`, `-fold`, etc., but no lazy iterator —\nno way to stop iteration early without hand-rolling an escape continuation.\n\n**7. The `assoc` bug lives in the collision-bucket path.**\npmap.sls:122:\n\n```scheme\n[(hamt-coll? node)\n (and (= (hamt-coll-hash node) key-hash)\n      (let ([p (assoc key (hamt-coll-pairs node) equal-proc)])  ; BUG\n        p))]\n```\n\nChez's `assoc` takes 2 args, not 3. SRFI-1's `assoc` takes an optional\n3rd `=` argument, but Chez's built-in does not. So on any true hash\ncollision where you try to look up the colliding key, this will raise a\nwrong-number-of-arguments error at runtime. The insert path at\npmap.sls:209 has the same bug. Fix is straightforward:\n\n```scheme\n;; replace (assoc key pairs equal-proc) with a hand-rolled find:\n(let loop ([ps pairs])\n  (cond\n    [(null? ps) #f]\n    [(equal-proc (caar ps) key) (car ps)]\n    [else (loop (cdr ps))]))\n```\n\nThis only bites on true hash collisions, which is why the compile warning\ndoesn't show up as an obvious runtime failure in normal testing. But once\nyou fix it, collision handling becomes correct.", "source": "doc:docs/thread-safe-hash.md:What Clojure has that Jerboa's pmap is m"}
 {"instruction": "Explain Practical takeaway in Jerboa Scheme.", "input": "", "output": "For a \"thread-safe hash-table-ish thing\" with light-to-moderate write rates,\nJerboa's pmap is functionally equivalent to Clojure's — same asymptotic\nbehavior, same thread-safety-by-immutability guarantee. The practical\ndifferences only start to bite at:\n\n- **Bulk construction** from large collections → you'll feel the lack of\n  transients (a 10× hit on `persistent-map-map` / `persistent-map-merge`\n  over millions of entries).\n- **Very large dense maps** → ArrayNode absence adds a constant factor to\n  every lookup, maybe 1.5–2× slower than Clojure at 1M+ entries.\n- **Collision-heavy workloads** → the `assoc` bug at pmap.sls:122 will\n  surface as a runtime crash. Fix it before shipping.\n\nFor the common case (thousands to tens of thousands of entries, moderate\nmutation rate, read-heavy), the gap between Jerboa's pmap and Clojure's PHM\nis not going to be your bottleneck. `imap + shared` remains the right\ndefault for a thread-safe hash-table-like value in Jerboa.", "source": "doc:docs/thread-safe-hash.md:Practical takeaway"}
 {"instruction": "Explain Jerboa Tutorial: Build a URL Shortener in Jerboa Scheme.", "input": "", "output": "This tutorial walks you from `hello world` to a deployable single-file\nJerboa service. By the end you will have built **Stubby**, a small URL\nshortener with:\n\n- A `defstruct`-modeled core\n- An in-memory cache and a SQLite persistent store\n- An HTTP API on top of `(std net httpd)`\n- A test suite run by `jerboa test`\n- A packaged, redistributable single-file script\n\nEvery code block in this document is a complete runnable or compilable\nJerboa program — the project's CI verifies that.\n\n---", "source": "doc:docs/tutorial.md:Jerboa Tutorial: Build a URL Shortener"}
-{"instruction": "Explain 0. Prerequisites in Jerboa Scheme.", "input": "", "output": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---", "source": "doc:docs/tutorial.md:0. Prerequisites"}
+{"instruction": "Explain 0. Prerequisites in Jerboa Scheme.", "input": "", "output": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---", "source": "doc:docs/tutorial.md:0. Prerequisites"}
 {"instruction": "Explain 1. Hello, world in Jerboa Scheme.", "input": "", "output": "Create `stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Stubby is starting up.\")\n```\n\nRun it:\n\n```bash\njerboa run stubby.ss\n```\n\nOne import, one line of code. `(jerboa prelude)` is the entire\nlanguage — no separate core/text/number modules to juggle. Everything\nyou'll use below (structs, hash tables, regex, JSON, format, match)\nlives in the prelude.\n\n---", "source": "doc:docs/tutorial.md:1. Hello, world"}
 {"instruction": "Explain 2. Model the domain with `defstruct` in Jerboa Scheme.", "input": "", "output": "A short link maps a **slug** (`abc123`) to a **target URL** and tracks\nhow many times it has been resolved.\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target)\n  (make-short-link slug target 0))\n\n(def (bump-hits! link)\n  (short-link-hits-set! link (+ (short-link-hits link) 1)))\n\n(let ([ln (new-link \"abc\" \"https://example.com\")])\n  (bump-hits! ln)\n  (bump-hits! ln)\n  (printf \"slug=~a target=~a hits=~a~n\"\n          (short-link-slug ln)\n          (short-link-target ln)\n          (short-link-hits ln)))\n```\n\n`defstruct` autogenerates `make-short-link`, `short-link?`, one\naccessor per field (`short-link-slug`), and mutators with the\n`-set!` suffix (`short-link-hits-set!`). No more boilerplate.\n\n---", "source": "doc:docs/tutorial.md:2. Model the domain with `defstruct`"}
 {"instruction": "Explain 3. Generate slugs in Jerboa Scheme.", "input": "", "output": "Real shorteners let users pass a custom slug, but fall back to a\nrandom one. We'll use the prelude's `random` and `string-append`:\n\n```scheme\n(import (jerboa prelude))\n\n(def *alphabet*\n  (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(displayln (random-slug))\n(displayln (random-slug 8))\n```\n\nNotes:\n- The ambiguous characters `0 o l 1` are omitted so slugs stay\n  copy-paste-safe over voice/print.\n- `(random N)` returns an integer in `[0, N)`.\n- `(iota n)` is `0..n-1` — the Jerboa prelude shadows Chez's\n  stricter version to give you the SRFI-1 interface.\n\n---", "source": "doc:docs/tutorial.md:3. Generate slugs"}
diff --git a/training_data_alpaca.v1.jsonl b/training_data_alpaca.v1.jsonl
index 82e9f9c..0ed9bac 100644
--- a/training_data_alpaca.v1.jsonl
+++ b/training_data_alpaca.v1.jsonl
@@ -35956,7 +35956,7 @@
 {"instruction": "Explain Tier 4 — Niche, deferred, or non-goal in Jerboa Scheme.", "input": "", "output": "| Feature | Section | Status | Reasoning |\n|---------|---------|--------|-----------|\n| **Clojure reader literals (`{}`/`#{}`/`[]`/`:kw`)** | [`clojure-reader.md`](./clojure-reader.md), [§4.9](./clojure-remaining.md#49-reader-literals-----v-kw) | Deferred (risky) | Requires opt-in file-local reader modes; permanent debuggability weirdness; most porters adapt to constructor forms in a day. Could be deferred forever. |\n| **Parked `go` via CPS transform** | [§10](#10-coreasync-csp-channels), §3.8 in `clojure-remaining.md` | Deferred | Research-scale macro project. Jerboa's OS-thread `go` is fine for thousands-of-coroutines use cases, just not millions. |\n| **Clojure Spec / full schema system** | [§17](#17-clojure-spec) | Non-goal | Too large to port faithfully. A scaled-down malli-style schema library is the recommended alternative for anyone who needs it. |\n| **core.logic (miniKanren)** | [§29](#29-corelogic-minikanren) | Open, low priority | A self-contained library port; valuable for anyone doing logic programming, but far from the Clojure-compat critical path. |\n| **Keywords as functions `(: k m)` on pmaps** | [§22](#22-keywordsymbol-semantics) | Deferred | Would require applicable-struct support. Instead use `(get m k)`, which is already polymorphic. |\n| **nREPL-style editor protocol** | [§37](#37-repl-driven-development-culture) | Partial | Jerboa's MCP tooling covers a lot of the same ground (`jerboa_eval`, `jerboa_repl_session`). A proper nREPL server would unlock CIDER/Calva-style editor integration but is a separate project. |\n| **Reader conditionals** | [§20](#20-reader-conditionals) | Deferred | Useful only when Jerboa has multiple targets; currently one target. Revisit if Jerboa-wasm or Gerbil-share becomes real. |\n| **`.clj` source loading** | — | Non-goal | Not porting the JVM compiler. |", "source": "doc:docs/clojure-vs-jerboa.md:Tier 4 — Niche, deferred, or non-goal"}
 {"instruction": "Explain Recommendation in Jerboa Scheme.", "input": "", "output": "Tier 1 is done. The next natural pass is **Tier 2 items in isolation** — lazy sequences, zippers, and Specter-style paths each stand alone and become more attractive now that the core ergonomics gap is closed. Tier 3's STM is the only remaining \"foundational\" gap and deserves a real design round of its own.\n\nThe language features above are what you'd port. The culture is built alongside, one idiomatic library at a time.", "source": "doc:docs/clojure-vs-jerboa.md:Recommendation"}
 {"instruction": "Explain Compiling Gerbil Projects with Jerboa in Jerboa Scheme.", "input": "", "output": "This guide walks through the complete process of taking an existing Gerbil\nScheme project and compiling it to run on Chez Scheme via Jerboa.", "source": "doc:docs/compiling-gerbil-projects.md:Compiling Gerbil Projects with Jerboa"}
-{"instruction": "Explain Prerequisites in Jerboa Scheme.", "input": "", "output": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://github.com/ober/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```", "source": "doc:docs/compiling-gerbil-projects.md:Prerequisites"}
+{"instruction": "Explain Prerequisites in Jerboa Scheme.", "input": "", "output": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://git.sr.ht/~lisp/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```", "source": "doc:docs/compiling-gerbil-projects.md:Prerequisites"}
 {"instruction": "Explain Project Layout in Jerboa Scheme.", "input": "", "output": "A jerboa project wraps an existing Gerbil project with build scripts and a\ncompatibility layer:\n\n```\nmy-jerboa-project/\n  my-gerbil-project/        # Gerbil source (submodule or copy)\n    gerbil.pkg\n    *.ss                     # Gerbil source files (never modified)\n  src/\n    compat/                  # Chez compatibility shims (handwritten)\n      sugar.sls              # :std/sugar equivalents\n      format.sls             # :std/format equivalents\n      ...\n    mylib/                   # Generated .sls files (gitignored)\n  build-jerboa.ss            # Jerboa translation driver\n  build-all.ss               # Chez compilation trigger\n  build-binary.ss            # Optional: standalone binary builder\n  main.ss                    # Entry point program\n  Makefile\n```", "source": "doc:docs/compiling-gerbil-projects.md:Project Layout"}
 {"instruction": "Explain Step 1: Create the Makefile in Jerboa Scheme.", "input": "", "output": "```makefile\nSCHEME = scheme\nJERBOA = $(or $(JERBOA_DIR),$(HOME)/src/jerboa/src)\nLIBDIRS = src:$(JERBOA)\nCOMPILE = $(SCHEME) -q --libdirs $(LIBDIRS) --compile-imported-libraries\n\n.PHONY: all jerboa compile binary run clean\n\nall: jerboa compile", "source": "doc:docs/compiling-gerbil-projects.md:Step 1: Create the Makefile"}
 {"instruction": "Explain Run interpreted in Jerboa Scheme.", "input": "", "output": "run: all\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program main.ss\n\nclean:\n\tfind src -name '*.so' -o -name '*.wpo' | xargs rm -f 2>/dev/null || true\n\trm -f src/mylib/*.sls\n```\n\nThe `JERBOA` variable points to jerboa's `src/` directory. Override it:\n```bash\nJERBOA_DIR=/path/to/jerboa/src make\n```\n\nIf your project depends on other jerboa-compiled libraries, add them to\n`LIBDIRS`:\n```makefile\nJERBOA_AWS = $(or $(JERBOA_AWS_DIR),jerboa-aws/src)\nLIBDIRS = src:$(JERBOA):$(JERBOA_AWS)\n```", "source": "doc:docs/compiling-gerbil-projects.md:Run interpreted"}
@@ -36646,7 +36646,7 @@
 {"instruction": "Explain Clojure Equivalent in Jerboa Scheme.", "input": "", "output": "To build the same webhook processor in Clojure, you need:\n\n```\nproject.clj (or deps.edn)          — dependency management\n├── JVM 17+                        — 200MB runtime\n├── ring/ring-core                 — HTTP abstraction\n├── http-kit or aleph              — async HTTP server\n├── compojure or reitit            — routing\n├── cheshire                       — JSON parsing\n├── core.async                     — channels\n├── mount or component             — lifecycle\n├── redis (Carmine)                — job queue\n├── sidekiq or machinery           — background workers\n├── timbre                         — logging\n└── Docker + docker-compose.yml    — deployment\n```\n\n**Result:**\n- 12+ dependencies (each with transitive deps → 100+ JARs)\n- 3 processes minimum (JVM app + Redis + worker process)\n- 30-second cold start (JVM + dependency loading)\n- 512MB-2GB RAM (JVM heap + Redis)\n- Docker image: 400MB-1GB\n- `docker-compose up` to run locally\n- 5-10 files of boilerplate before writing business logic", "source": "doc:docs/jerboa-edge.md:Clojure Equivalent"}
 {"instruction": "Explain Jerboa Equivalent in Jerboa Scheme.", "input": "", "output": "```\nwebhook-service.ss                  — the entire service\n```\n\n**Result:**\n- 0 external dependencies (everything is stdlib)\n- 1 process (one binary does HTTP + workers + state + WebSocket)\n- Instant startup (Chez native code, no interpreter warmup)\n- 20-40MB RAM (no GC pressure from boxed JVM objects)\n- Static binary: 15MB\n- `scp edge user@server: && ssh server ./edge` to deploy\n- ~380 lines, one file, zero config files", "source": "doc:docs/jerboa-edge.md:Jerboa Equivalent"}
 {"instruction": "Explain Side-by-Side: Processing a Webhook in Jerboa Scheme.", "input": "", "output": "**Clojure:**\n```clojure\n;; deps: ring, compojure, cheshire, core.async, carmine (redis)\n\n(ns webhook.handler\n  (:require [compojure.core :refer [defroutes POST]]\n            [ring.middleware.json :refer [wrap-json-body]]\n            [cheshire.core :as json]\n            [clojure.core.async :as async]\n            [taoensso.carmine :as redis]))\n\n(defroutes app-routes\n  (POST \"/hooks/:type\" [type :as req]\n    (let [payload (:body req)]\n      ;; Push to Redis queue (external process!)\n      (redis/wcar {} (redis/lpush \"webhooks\" (json/encode payload)))\n      {:status 202\n       :body (json/encode {:status \"queued\"})})))\n\n;; Separate worker process reads from Redis:\n(defn worker []\n  (loop []\n    (let [raw (redis/wcar {} (redis/brpop \"webhooks\" 0))\n          event (json/decode (second raw) true)]\n      (process-event! event)\n      (recur))))\n```\n\n**Jerboa:**\n```scheme\n;; deps: none (everything is stdlib)\n\n(import (jerboa prelude)\n        (std clojure)\n        (std net fiber-httpd)\n        (std csp clj)\n        (std text json))\n\n(def work-ch (chan 4096))\n\n(def (handle-webhook req)\n  (let ([payload (string->json-object (request-body req))])\n    (>!! work-ch payload)\n    (respond-json 202 \"{\\\"status\\\":\\\"queued\\\"}\")))\n\n;; Workers are fibers on the same process — no Redis, no IPC\n(dotimes (i 4)\n  (go (let loop ()\n        (let ([event (<!! work-ch)])\n          (when event\n            (process-event! event)\n            (loop))))))\n\n(fiber-httpd-start 8080 handle-webhook)\n```\n\nThe Jerboa version is **12 lines** vs Clojure's **20+ lines + Redis + a separate\nworker process**.  And the Jerboa version handles more concurrent connections\n(fibers vs threads), has built-in fault tolerance (supervisor), and deploys as\none file.\n\n---", "source": "doc:docs/jerboa-edge.md:Side-by-Side: Processing a Webhook"}
-{"instruction": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme.", "input": "", "output": "**Deliverable:** [`jerboa-edge/edge.ss`](https://github.com/ober/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://github.com/ober/jerboa-edge\ncd jerboa-edge\nmake run", "source": "doc:docs/jerboa-edge.md:Phase 1: Core Demo (the ~380-line file) "}
+{"instruction": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme.", "input": "", "output": "**Deliverable:** [`jerboa-edge/edge.ss`](https://git.sr.ht/~lisp/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa-edge\ncd jerboa-edge\nmake run", "source": "doc:docs/jerboa-edge.md:Phase 1: Core Demo (the ~380-line file) "}
 {"instruction": "Explain In another terminal: in Jerboa Scheme.", "input": "", "output": "curl -X POST localhost:8080/hooks/payment.completed \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"evt_123\",\"amount\":4999}'\n\ncurl localhost:8080/api/stats", "source": "doc:docs/jerboa-edge.md:In another terminal:"}
 {"instruction": "Explain => {\"total\":1,\"ok\":1,\"error\":0} in Jerboa Scheme.", "input": "", "output": "```\n\n**Scope:**\n\n- [x] 1.1 **HTTP server with routing** — fiber-httpd with `:param` pattern routing.\n    Five endpoints: `POST /hooks/:type`, `GET /api/events/:id`, `GET /api/stats`,\n    `GET /health`, `GET /dashboard`.\n\n- [x] 1.2 **Channel-based ingestion** — CSP channel with transducer pipeline\n    (validate + dedup-by-ID + normalize).  Decouples accept from process.\n\n- [x] 1.3 **Actor-supervised workers** — N worker actors under a one-for-one\n    supervisor.  Each pulls from the channel, processes, writes to state.\n    `POST /hooks/test.crash` deliberately crashes a worker to show supervisor restart.\n\n- [x] 1.4 **STM state store** — All state in `(make-ref (make-persistent-map))`.\n    Atomic updates via `dosync`/`alter` with `(std pmap)` persistent maps.\n    Immutable snapshots — safe for concurrent reads during writes.\n\n- [x] 1.5 **WebSocket dashboard** — Single WS endpoint that streams\n    event-processed notifications to connected clients.  Sends initial stats\n    on connect.\n\n- [x] 1.6 **HMAC signature verification** — Validate `X-Signature` header using\n    `(std crypto native)` HMAC-SHA256.  Constant-time comparison.\n    Configurable via `EDGE_SECRET` env var.\n\n**Non-goals for Phase 1:**\n- Persistence across restarts (in-memory only)\n- TLS termination (use a reverse proxy, or wait for Phase 3)\n- Static binary packaging (Phase 3)\n\n**Success criteria:**\n- [x] Runs with `make run` (382 lines of Scheme)\n- [x] Handles 10K concurrent webhook POSTs without dropped events (verified: 9,993/10,000 accepted at 200 concurrent connections, ~454 req/sec)\n- [x] Worker crash + supervisor restart visible in logs\n- [x] WebSocket dashboard receives live event stream (verified with websocket-client)\n- [x] `curl` examples work out of the box (17/17 smoke tests pass)\n- [x] HMAC: no-sig and wrong-sig return 401, valid-sig returns 202", "source": "doc:docs/jerboa-edge.md:=> {\"total\":1,\"ok\":1,\"error\":0}"}
 {"instruction": "Explain Phase 2: Programmability in Jerboa Scheme.", "input": "", "output": "**Deliverable:** Sandboxed user filter support + handler registry\n\n- [x] 2.1 **Sandboxed eval** — Users submit filter expressions via\n    `POST /api/filters`.  Filters evaluated per-event via\n    `restricted-eval-string` (allowlist-only: no I/O, no FFI) with a\n    2-second engine-based timeout (no fork — fiber-safe).  Events that\n    fail any filter are dropped silently before processing.\n\n    ```\n    POST /api/filters  {\"name\":\"payments-only\",\"code\":\"(equal? (cdr (assoc \\\"type\\\" evt)) \\\"payment.completed\\\")\"}\n    GET  /api/filters\n    DELETE /api/filters/:name\n    ```\n\n    Key implementation detail: event data is bound as `evt` (a deep\n    alist), so filter code uses `assoc` with string keys (not `assq`).\n\n- [x] 2.2 **Hot handler registration** — New webhook types registered at\n    runtime via `POST /api/handlers`.  No restart required.  Handler\n    code runs in a custom restricted environment (`safe-bindings` +\n    `displayln`) with a 5-second engine timeout.  Exceptions propagate\n    to the worker's retry machinery.\n\n    ```\n    POST /api/handlers  {\"type\":\"order.shipped\",\"code\":\"(displayln \\\"shipped: \\\" (cdr (assoc \\\"id\\\" evt)))\"}\n    GET  /api/handlers\n    ```\n\n- [x] 2.3 **Retry with exponential backoff** — Failed events re-enter the\n    channel with an incremented retry count.  Workers delay via\n    `fork-thread` + `sleep` (non-blocking, OS-thread safe).  Dedup\n    transducer passes retried events (`retry-count > 0`).  After 3\n    retries, events go to a dead-letter STM store.\n\n    ```\n    GET  /api/dead-letter\n    POST /api/dead-letter/:id/replay\n    ```\n\n    Replay sets `retry-count: 1` to bypass the dedup filter.\n\n**Success criteria:**\n- [x] Filter code cannot call `open-file`, `socket`, or any I/O (allowlist env)\n- [x] Filter with 2s timeout verified: `with-timeout` uses Chez engines, no fork\n- [x] Hot-registered handlers process events without service restart (verified: `order.shipped`, `inventory.updated`)\n- [x] Failed events retry 3× with 1s/2s/4s backoff then dead-letter (verified in log)\n- [x] Dead-letter replay re-processes event and clears queue (verified: status → \"ok\")\n- [x] Runs with `make run` (690 lines of Scheme)", "source": "doc:docs/jerboa-edge.md:Phase 2: Programmability"}
@@ -36947,7 +36947,7 @@
 {"instruction": "Explain Streaming with ports in Jerboa Scheme.", "input": "", "output": "```scheme\n(import (std text msgpack))\n\n;; Write multiple values to a port\n(let-values ([(port extract) (open-bytevector-output-port)])\n  (msgpack-pack-port 1 port)\n  (msgpack-pack-port \"hello\" port)\n  (msgpack-pack-port (vector 10 20) port)\n  (let* ([bv (extract)]\n         [in (open-bytevector-input-port bv)])\n    (list\n      (msgpack-unpack-port in)    ;=> 1\n      (msgpack-unpack-port in)    ;=> \"hello\"\n      (msgpack-unpack-port in)))) ;=> #(10 20)\n;=> (1 \"hello\" #(10 20))\n```", "source": "doc:docs/protocols.md:Streaming with ports"}
 {"instruction": "Explain Integer encoding compactness in Jerboa Scheme.", "input": "", "output": "The encoder automatically picks the smallest representation:\n\n```scheme\n(import (std text msgpack))\n\n;; Positive fixint (1 byte for 0-127)\n(bytevector-length (msgpack-pack 0))    ;=> 1\n(bytevector-length (msgpack-pack 127))  ;=> 1\n\n;; Negative fixint (1 byte for -32 to -1)\n(bytevector-length (msgpack-pack -1))   ;=> 1\n(bytevector-length (msgpack-pack -32))  ;=> 1\n\n;; uint8 (2 bytes for 128-255)\n(bytevector-length (msgpack-pack 200))  ;=> 2\n\n;; uint16 (3 bytes for 256-65535)\n(bytevector-length (msgpack-pack 1000)) ;=> 3\n```", "source": "doc:docs/protocols.md:Integer encoding compactness"}
 {"instruction": "Explain Interoperability in Jerboa Scheme.", "input": "", "output": "MessagePack is a cross-language format. Bytevectors produced by `msgpack-pack` are compatible with any MessagePack implementation (Python, Go, Rust, JavaScript, etc.) and vice versa.\n\n```scheme\n(import (std text msgpack))\n\n;; Encode a structure for sending over the network\n(define request\n  (msgpack-pack\n    '((\"method\" . \"getUser\")\n      (\"params\" . #(42))\n      (\"id\" . 1))))\n\n;; request is a compact bytevector ready for transmission\n(bytevector-length request)  ; much smaller than equivalent JSON\n```", "source": "doc:docs/protocols.md:Interoperability"}
-{"instruction": "Explain Jerboa Quickstart in Jerboa Scheme.", "input": "", "output": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs", "source": "doc:docs/quickstart.md:full"}
+{"instruction": "Explain Jerboa Quickstart in Jerboa Scheme.", "input": "", "output": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs", "source": "doc:docs/quickstart.md:full"}
 {"instruction": "Explain 3. Hello World in Jerboa Scheme.", "input": "", "output": "Create `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```", "source": "doc:docs/quickstart.md:3. Hello World"}
 {"instruction": "Explain 4. Use the REPL in Jerboa Scheme.", "input": "", "output": "```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.", "source": "doc:docs/quickstart.md:4. Use the REPL"}
 {"instruction": "Explain 5. Define a Struct in Jerboa Scheme.", "input": "", "output": "```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```", "source": "doc:docs/quickstart.md:5. Define a Struct"}
@@ -37328,7 +37328,7 @@
 {"instruction": "Explain What Clojure has that Jerboa's pmap is missing in Jerboa Scheme.", "input": "", "output": "**1. Transients — the big one.**\nClojure lets you \"thaw\" a persistent map into a transient, do a bulk of\nmutations (`assoc!`, `dissoc!`) with near-mutable cost because only one\nowner is assumed, then freeze it back (`persistent!`). Building a 100k-entry\nmap from an alist is ~4–8× faster with transients than with repeated\n`assoc`. Jerboa's pmap has no transient mechanism — every `imap-set` inside\n`persistent-map-map` / `persistent-map-filter` / `persistent-map-merge`\nallocates a full path copy. See pmap.sls:344-358: `persistent-map-map`\nliterally does `(set! result (persistent-map-set result k (proc k v)))` in\na loop, one allocation-heavy step at a time.\n\n**2. No ArrayNode (dense-node optimization).**\nClojure has *two* interior-node types: `BitmapIndexedNode` for sparse nodes\nand `ArrayNode` for dense ones. When a bitmap-indexed node would reach ~16\nchildren, Clojure promotes it to a 32-slot flat array with `nil` in empty\nslots. Lookups in an ArrayNode skip the popcount indirection — you go\nstraight to `array[hash_chunk]`. Jerboa only has the bitmap variant, so\nevery lookup pays the popcount cost at every level, even in hot dense\nregions of the tree.\n\n**3. Leaves stored inline, not boxed.**\nClojure's `BitmapIndexedNode` stores `[k1, v1, k2, v2, …]` (or\n`[nil, child]` for nested nodes) directly in the node's single array.\nJerboa allocates a separate `hamt-leaf` record for every leaf and stores a\npointer to it in the parent's array (pmap.sls:51-52, used throughout). For\na 1M-entry map that's 1M extra heap objects and one extra pointer\nindirection on every successful lookup.\n\n**4. A hash function picked for HAMT.**\nClojure uses Murmur3 (and `mix-collection-hash`) specifically because HAMT\nperformance is very sensitive to the uniformity of the low bits. Jerboa\npmap uses Chez's stock `equal-hash`, which is a reasonable general-purpose\nhash but wasn't designed for this. In practice it's probably fine; in\npathological distributions it could mean deeper trees.\n\n**5. Structural equality and hashing.**\nA Clojure map *is* hashable by contents, so you can use a map as a key in\nanother map, and `(= m1 m2)` compares contents. Jerboa's `%pmap` is a plain\nrecord — it uses identity equality by default, so you can't use an `imap`\nas a key in an `imap` and `equal?` on two pmaps won't do what you want.\n\n**6. Seq abstraction / lazy iteration.**\nClojure maps participate in the `seq` / `reduce` / `transducer` abstractions\nfor free, including early termination via `reduced`. Jerboa provides\n`persistent-map-for-each`, `->list`, `-fold`, etc., but no lazy iterator —\nno way to stop iteration early without hand-rolling an escape continuation.\n\n**7. The `assoc` bug lives in the collision-bucket path.**\npmap.sls:122:\n\n```scheme\n[(hamt-coll? node)\n (and (= (hamt-coll-hash node) key-hash)\n      (let ([p (assoc key (hamt-coll-pairs node) equal-proc)])  ; BUG\n        p))]\n```\n\nChez's `assoc` takes 2 args, not 3. SRFI-1's `assoc` takes an optional\n3rd `=` argument, but Chez's built-in does not. So on any true hash\ncollision where you try to look up the colliding key, this will raise a\nwrong-number-of-arguments error at runtime. The insert path at\npmap.sls:209 has the same bug. Fix is straightforward:\n\n```scheme\n;; replace (assoc key pairs equal-proc) with a hand-rolled find:\n(let loop ([ps pairs])\n  (cond\n    [(null? ps) #f]\n    [(equal-proc (caar ps) key) (car ps)]\n    [else (loop (cdr ps))]))\n```\n\nThis only bites on true hash collisions, which is why the compile warning\ndoesn't show up as an obvious runtime failure in normal testing. But once\nyou fix it, collision handling becomes correct.", "source": "doc:docs/thread-safe-hash.md:What Clojure has that Jerboa's pmap is m"}
 {"instruction": "Explain Practical takeaway in Jerboa Scheme.", "input": "", "output": "For a \"thread-safe hash-table-ish thing\" with light-to-moderate write rates,\nJerboa's pmap is functionally equivalent to Clojure's — same asymptotic\nbehavior, same thread-safety-by-immutability guarantee. The practical\ndifferences only start to bite at:\n\n- **Bulk construction** from large collections → you'll feel the lack of\n  transients (a 10× hit on `persistent-map-map` / `persistent-map-merge`\n  over millions of entries).\n- **Very large dense maps** → ArrayNode absence adds a constant factor to\n  every lookup, maybe 1.5–2× slower than Clojure at 1M+ entries.\n- **Collision-heavy workloads** → the `assoc` bug at pmap.sls:122 will\n  surface as a runtime crash. Fix it before shipping.\n\nFor the common case (thousands to tens of thousands of entries, moderate\nmutation rate, read-heavy), the gap between Jerboa's pmap and Clojure's PHM\nis not going to be your bottleneck. `imap + shared` remains the right\ndefault for a thread-safe hash-table-like value in Jerboa.", "source": "doc:docs/thread-safe-hash.md:Practical takeaway"}
 {"instruction": "Explain Jerboa Tutorial: Build a URL Shortener in Jerboa Scheme.", "input": "", "output": "This tutorial walks you from `hello world` to a deployable single-file\nJerboa service. By the end you will have built **Stubby**, a small URL\nshortener with:\n\n- A `defstruct`-modeled core\n- An in-memory cache and a SQLite persistent store\n- An HTTP API on top of `(std net httpd)`\n- A test suite run by `jerboa test`\n- A packaged, redistributable single-file script\n\nEvery code block in this document is a complete runnable or compilable\nJerboa program — the project's CI verifies that.\n\n---", "source": "doc:docs/tutorial.md:Jerboa Tutorial: Build a URL Shortener"}
-{"instruction": "Explain 0. Prerequisites in Jerboa Scheme.", "input": "", "output": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---", "source": "doc:docs/tutorial.md:0. Prerequisites"}
+{"instruction": "Explain 0. Prerequisites in Jerboa Scheme.", "input": "", "output": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---", "source": "doc:docs/tutorial.md:0. Prerequisites"}
 {"instruction": "Explain 1. Hello, world in Jerboa Scheme.", "input": "", "output": "Create `stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Stubby is starting up.\")\n```\n\nRun it:\n\n```bash\njerboa run stubby.ss\n```\n\nOne import, one line of code. `(jerboa prelude)` is the entire\nlanguage — no separate core/text/number modules to juggle. Everything\nyou'll use below (structs, hash tables, regex, JSON, format, match)\nlives in the prelude.\n\n---", "source": "doc:docs/tutorial.md:1. Hello, world"}
 {"instruction": "Explain 2. Model the domain with `defstruct` in Jerboa Scheme.", "input": "", "output": "A short link maps a **slug** (`abc123`) to a **target URL** and tracks\nhow many times it has been resolved.\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target)\n  (make-short-link slug target 0))\n\n(def (bump-hits! link)\n  (short-link-hits-set! link (+ (short-link-hits link) 1)))\n\n(let ([ln (new-link \"abc\" \"https://example.com\")])\n  (bump-hits! ln)\n  (bump-hits! ln)\n  (printf \"slug=~a target=~a hits=~a~n\"\n          (short-link-slug ln)\n          (short-link-target ln)\n          (short-link-hits ln)))\n```\n\n`defstruct` autogenerates `make-short-link`, `short-link?`, one\naccessor per field (`short-link-slug`), and mutators with the\n`-set!` suffix (`short-link-hits-set!`). No more boilerplate.\n\n---", "source": "doc:docs/tutorial.md:2. Model the domain with `defstruct`"}
 {"instruction": "Explain 3. Generate slugs in Jerboa Scheme.", "input": "", "output": "Real shorteners let users pass a custom slug, but fall back to a\nrandom one. We'll use the prelude's `random` and `string-append`:\n\n```scheme\n(import (jerboa prelude))\n\n(def *alphabet*\n  (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(displayln (random-slug))\n(displayln (random-slug 8))\n```\n\nNotes:\n- The ambiguous characters `0 o l 1` are omitted so slugs stay\n  copy-paste-safe over voice/print.\n- `(random N)` returns an integer in `[0, N)`.\n- `(iota n)` is `0..n-1` — the Jerboa prelude shadows Chez's\n  stricter version to give you the SRFI-1 interface.\n\n---", "source": "doc:docs/tutorial.md:3. Generate slugs"}
diff --git a/training_data_together.jsonl b/training_data_together.jsonl
index e01d47f..cbcc7be 100644
--- a/training_data_together.jsonl
+++ b/training_data_together.jsonl
@@ -36262,7 +36262,7 @@
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Tier 4 — Niche, deferred, or non-goal in Jerboa Scheme."}, {"role": "assistant", "content": "| Feature | Section | Status | Reasoning |\n|---------|---------|--------|-----------|\n| **Clojure reader literals (`{}`/`#{}`/`[]`/`:kw`)** | [`clojure-reader.md`](./clojure-reader.md), [§4.9](./clojure-remaining.md#49-reader-literals-----v-kw) | Deferred (risky) | Requires opt-in file-local reader modes; permanent debuggability weirdness; most porters adapt to constructor forms in a day. Could be deferred forever. |\n| **Parked `go` via CPS transform** | [§10](#10-coreasync-csp-channels), §3.8 in `clojure-remaining.md` | Deferred | Research-scale macro project. Jerboa's OS-thread `go` is fine for thousands-of-coroutines use cases, just not millions. |\n| **Clojure Spec / full schema system** | [§17](#17-clojure-spec) | Non-goal | Too large to port faithfully. A scaled-down malli-style schema library is the recommended alternative for anyone who needs it. |\n| **core.logic (miniKanren)** | [§29](#29-corelogic-minikanren) | Open, low priority | A self-contained library port; valuable for anyone doing logic programming, but far from the Clojure-compat critical path. |\n| **Keywords as functions `(: k m)` on pmaps** | [§22](#22-keywordsymbol-semantics) | Deferred | Would require applicable-struct support. Instead use `(get m k)`, which is already polymorphic. |\n| **nREPL-style editor protocol** | [§37](#37-repl-driven-development-culture) | Partial | Jerboa's MCP tooling covers a lot of the same ground (`jerboa_eval`, `jerboa_repl_session`). A proper nREPL server would unlock CIDER/Calva-style editor integration but is a separate project. |\n| **Reader conditionals** | [§20](#20-reader-conditionals) | Deferred | Useful only when Jerboa has multiple targets; currently one target. Revisit if Jerboa-wasm or Gerbil-share becomes real. |\n| **`.clj` source loading** | — | Non-goal | Not porting the JVM compiler. |"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Recommendation in Jerboa Scheme."}, {"role": "assistant", "content": "Tier 1 is done. The next natural pass is **Tier 2 items in isolation** — lazy sequences, zippers, and Specter-style paths each stand alone and become more attractive now that the core ergonomics gap is closed. Tier 3's STM is the only remaining \"foundational\" gap and deserves a real design round of its own.\n\nThe language features above are what you'd port. The culture is built alongside, one idiomatic library at a time."}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Compiling Gerbil Projects with Jerboa in Jerboa Scheme."}, {"role": "assistant", "content": "This guide walks through the complete process of taking an existing Gerbil\nScheme project and compiling it to run on Chez Scheme via Jerboa."}]}
-{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://github.com/ober/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```"}]}
+{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://git.sr.ht/~lisp/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Project Layout in Jerboa Scheme."}, {"role": "assistant", "content": "A jerboa project wraps an existing Gerbil project with build scripts and a\ncompatibility layer:\n\n```\nmy-jerboa-project/\n  my-gerbil-project/        # Gerbil source (submodule or copy)\n    gerbil.pkg\n    *.ss                     # Gerbil source files (never modified)\n  src/\n    compat/                  # Chez compatibility shims (handwritten)\n      sugar.sls              # :std/sugar equivalents\n      format.sls             # :std/format equivalents\n      ...\n    mylib/                   # Generated .sls files (gitignored)\n  build-jerboa.ss            # Jerboa translation driver\n  build-all.ss               # Chez compilation trigger\n  build-binary.ss            # Optional: standalone binary builder\n  main.ss                    # Entry point program\n  Makefile\n```"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Step 1: Create the Makefile in Jerboa Scheme."}, {"role": "assistant", "content": "```makefile\nSCHEME = scheme\nJERBOA = $(or $(JERBOA_DIR),$(HOME)/src/jerboa/src)\nLIBDIRS = src:$(JERBOA)\nCOMPILE = $(SCHEME) -q --libdirs $(LIBDIRS) --compile-imported-libraries\n\n.PHONY: all jerboa compile binary run clean\n\nall: jerboa compile"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Run interpreted in Jerboa Scheme."}, {"role": "assistant", "content": "run: all\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program main.ss\n\nclean:\n\tfind src -name '*.so' -o -name '*.wpo' | xargs rm -f 2>/dev/null || true\n\trm -f src/mylib/*.sls\n```\n\nThe `JERBOA` variable points to jerboa's `src/` directory. Override it:\n```bash\nJERBOA_DIR=/path/to/jerboa/src make\n```\n\nIf your project depends on other jerboa-compiled libraries, add them to\n`LIBDIRS`:\n```makefile\nJERBOA_AWS = $(or $(JERBOA_AWS_DIR),jerboa-aws/src)\nLIBDIRS = src:$(JERBOA):$(JERBOA_AWS)\n```"}]}
@@ -36952,7 +36952,7 @@
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Clojure Equivalent in Jerboa Scheme."}, {"role": "assistant", "content": "To build the same webhook processor in Clojure, you need:\n\n```\nproject.clj (or deps.edn)          — dependency management\n├── JVM 17+                        — 200MB runtime\n├── ring/ring-core                 — HTTP abstraction\n├── http-kit or aleph              — async HTTP server\n├── compojure or reitit            — routing\n├── cheshire                       — JSON parsing\n├── core.async                     — channels\n├── mount or component             — lifecycle\n├── redis (Carmine)                — job queue\n├── sidekiq or machinery           — background workers\n├── timbre                         — logging\n└── Docker + docker-compose.yml    — deployment\n```\n\n**Result:**\n- 12+ dependencies (each with transitive deps → 100+ JARs)\n- 3 processes minimum (JVM app + Redis + worker process)\n- 30-second cold start (JVM + dependency loading)\n- 512MB-2GB RAM (JVM heap + Redis)\n- Docker image: 400MB-1GB\n- `docker-compose up` to run locally\n- 5-10 files of boilerplate before writing business logic"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Equivalent in Jerboa Scheme."}, {"role": "assistant", "content": "```\nwebhook-service.ss                  — the entire service\n```\n\n**Result:**\n- 0 external dependencies (everything is stdlib)\n- 1 process (one binary does HTTP + workers + state + WebSocket)\n- Instant startup (Chez native code, no interpreter warmup)\n- 20-40MB RAM (no GC pressure from boxed JVM objects)\n- Static binary: 15MB\n- `scp edge user@server: && ssh server ./edge` to deploy\n- ~380 lines, one file, zero config files"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Side-by-Side: Processing a Webhook in Jerboa Scheme."}, {"role": "assistant", "content": "**Clojure:**\n```clojure\n;; deps: ring, compojure, cheshire, core.async, carmine (redis)\n\n(ns webhook.handler\n  (:require [compojure.core :refer [defroutes POST]]\n            [ring.middleware.json :refer [wrap-json-body]]\n            [cheshire.core :as json]\n            [clojure.core.async :as async]\n            [taoensso.carmine :as redis]))\n\n(defroutes app-routes\n  (POST \"/hooks/:type\" [type :as req]\n    (let [payload (:body req)]\n      ;; Push to Redis queue (external process!)\n      (redis/wcar {} (redis/lpush \"webhooks\" (json/encode payload)))\n      {:status 202\n       :body (json/encode {:status \"queued\"})})))\n\n;; Separate worker process reads from Redis:\n(defn worker []\n  (loop []\n    (let [raw (redis/wcar {} (redis/brpop \"webhooks\" 0))\n          event (json/decode (second raw) true)]\n      (process-event! event)\n      (recur))))\n```\n\n**Jerboa:**\n```scheme\n;; deps: none (everything is stdlib)\n\n(import (jerboa prelude)\n        (std clojure)\n        (std net fiber-httpd)\n        (std csp clj)\n        (std text json))\n\n(def work-ch (chan 4096))\n\n(def (handle-webhook req)\n  (let ([payload (string->json-object (request-body req))])\n    (>!! work-ch payload)\n    (respond-json 202 \"{\\\"status\\\":\\\"queued\\\"}\")))\n\n;; Workers are fibers on the same process — no Redis, no IPC\n(dotimes (i 4)\n  (go (let loop ()\n        (let ([event (<!! work-ch)])\n          (when event\n            (process-event! event)\n            (loop))))))\n\n(fiber-httpd-start 8080 handle-webhook)\n```\n\nThe Jerboa version is **12 lines** vs Clojure's **20+ lines + Redis + a separate\nworker process**.  And the Jerboa version handles more concurrent connections\n(fibers vs threads), has built-in fault tolerance (supervisor), and deploys as\none file.\n\n---"}]}
-{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** [`jerboa-edge/edge.ss`](https://github.com/ober/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://github.com/ober/jerboa-edge\ncd jerboa-edge\nmake run"}]}
+{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** [`jerboa-edge/edge.ss`](https://git.sr.ht/~lisp/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa-edge\ncd jerboa-edge\nmake run"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain In another terminal: in Jerboa Scheme."}, {"role": "assistant", "content": "curl -X POST localhost:8080/hooks/payment.completed \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"evt_123\",\"amount\":4999}'\n\ncurl localhost:8080/api/stats"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain => {\"total\":1,\"ok\":1,\"error\":0} in Jerboa Scheme."}, {"role": "assistant", "content": "```\n\n**Scope:**\n\n- [x] 1.1 **HTTP server with routing** — fiber-httpd with `:param` pattern routing.\n    Five endpoints: `POST /hooks/:type`, `GET /api/events/:id`, `GET /api/stats`,\n    `GET /health`, `GET /dashboard`.\n\n- [x] 1.2 **Channel-based ingestion** — CSP channel with transducer pipeline\n    (validate + dedup-by-ID + normalize).  Decouples accept from process.\n\n- [x] 1.3 **Actor-supervised workers** — N worker actors under a one-for-one\n    supervisor.  Each pulls from the channel, processes, writes to state.\n    `POST /hooks/test.crash` deliberately crashes a worker to show supervisor restart.\n\n- [x] 1.4 **STM state store** — All state in `(make-ref (make-persistent-map))`.\n    Atomic updates via `dosync`/`alter` with `(std pmap)` persistent maps.\n    Immutable snapshots — safe for concurrent reads during writes.\n\n- [x] 1.5 **WebSocket dashboard** — Single WS endpoint that streams\n    event-processed notifications to connected clients.  Sends initial stats\n    on connect.\n\n- [x] 1.6 **HMAC signature verification** — Validate `X-Signature` header using\n    `(std crypto native)` HMAC-SHA256.  Constant-time comparison.\n    Configurable via `EDGE_SECRET` env var.\n\n**Non-goals for Phase 1:**\n- Persistence across restarts (in-memory only)\n- TLS termination (use a reverse proxy, or wait for Phase 3)\n- Static binary packaging (Phase 3)\n\n**Success criteria:**\n- [x] Runs with `make run` (382 lines of Scheme)\n- [x] Handles 10K concurrent webhook POSTs without dropped events (verified: 9,993/10,000 accepted at 200 concurrent connections, ~454 req/sec)\n- [x] Worker crash + supervisor restart visible in logs\n- [x] WebSocket dashboard receives live event stream (verified with websocket-client)\n- [x] `curl` examples work out of the box (17/17 smoke tests pass)\n- [x] HMAC: no-sig and wrong-sig return 401, valid-sig returns 202"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 2: Programmability in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** Sandboxed user filter support + handler registry\n\n- [x] 2.1 **Sandboxed eval** — Users submit filter expressions via\n    `POST /api/filters`.  Filters evaluated per-event via\n    `restricted-eval-string` (allowlist-only: no I/O, no FFI) with a\n    2-second engine-based timeout (no fork — fiber-safe).  Events that\n    fail any filter are dropped silently before processing.\n\n    ```\n    POST /api/filters  {\"name\":\"payments-only\",\"code\":\"(equal? (cdr (assoc \\\"type\\\" evt)) \\\"payment.completed\\\")\"}\n    GET  /api/filters\n    DELETE /api/filters/:name\n    ```\n\n    Key implementation detail: event data is bound as `evt` (a deep\n    alist), so filter code uses `assoc` with string keys (not `assq`).\n\n- [x] 2.2 **Hot handler registration** — New webhook types registered at\n    runtime via `POST /api/handlers`.  No restart required.  Handler\n    code runs in a custom restricted environment (`safe-bindings` +\n    `displayln`) with a 5-second engine timeout.  Exceptions propagate\n    to the worker's retry machinery.\n\n    ```\n    POST /api/handlers  {\"type\":\"order.shipped\",\"code\":\"(displayln \\\"shipped: \\\" (cdr (assoc \\\"id\\\" evt)))\"}\n    GET  /api/handlers\n    ```\n\n- [x] 2.3 **Retry with exponential backoff** — Failed events re-enter the\n    channel with an incremented retry count.  Workers delay via\n    `fork-thread` + `sleep` (non-blocking, OS-thread safe).  Dedup\n    transducer passes retried events (`retry-count > 0`).  After 3\n    retries, events go to a dead-letter STM store.\n\n    ```\n    GET  /api/dead-letter\n    POST /api/dead-letter/:id/replay\n    ```\n\n    Replay sets `retry-count: 1` to bypass the dedup filter.\n\n**Success criteria:**\n- [x] Filter code cannot call `open-file`, `socket`, or any I/O (allowlist env)\n- [x] Filter with 2s timeout verified: `with-timeout` uses Chez engines, no fork\n- [x] Hot-registered handlers process events without service restart (verified: `order.shipped`, `inventory.updated`)\n- [x] Failed events retry 3× with 1s/2s/4s backoff then dead-letter (verified in log)\n- [x] Dead-letter replay re-processes event and clears queue (verified: status → \"ok\")\n- [x] Runs with `make run` (690 lines of Scheme)"}]}
@@ -37253,7 +37253,7 @@
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Streaming with ports in Jerboa Scheme."}, {"role": "assistant", "content": "```scheme\n(import (std text msgpack))\n\n;; Write multiple values to a port\n(let-values ([(port extract) (open-bytevector-output-port)])\n  (msgpack-pack-port 1 port)\n  (msgpack-pack-port \"hello\" port)\n  (msgpack-pack-port (vector 10 20) port)\n  (let* ([bv (extract)]\n         [in (open-bytevector-input-port bv)])\n    (list\n      (msgpack-unpack-port in)    ;=> 1\n      (msgpack-unpack-port in)    ;=> \"hello\"\n      (msgpack-unpack-port in)))) ;=> #(10 20)\n;=> (1 \"hello\" #(10 20))\n```"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Integer encoding compactness in Jerboa Scheme."}, {"role": "assistant", "content": "The encoder automatically picks the smallest representation:\n\n```scheme\n(import (std text msgpack))\n\n;; Positive fixint (1 byte for 0-127)\n(bytevector-length (msgpack-pack 0))    ;=> 1\n(bytevector-length (msgpack-pack 127))  ;=> 1\n\n;; Negative fixint (1 byte for -32 to -1)\n(bytevector-length (msgpack-pack -1))   ;=> 1\n(bytevector-length (msgpack-pack -32))  ;=> 1\n\n;; uint8 (2 bytes for 128-255)\n(bytevector-length (msgpack-pack 200))  ;=> 2\n\n;; uint16 (3 bytes for 256-65535)\n(bytevector-length (msgpack-pack 1000)) ;=> 3\n```"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Interoperability in Jerboa Scheme."}, {"role": "assistant", "content": "MessagePack is a cross-language format. Bytevectors produced by `msgpack-pack` are compatible with any MessagePack implementation (Python, Go, Rust, JavaScript, etc.) and vice versa.\n\n```scheme\n(import (std text msgpack))\n\n;; Encode a structure for sending over the network\n(define request\n  (msgpack-pack\n    '((\"method\" . \"getUser\")\n      (\"params\" . #(42))\n      (\"id\" . 1))))\n\n;; request is a compact bytevector ready for transmission\n(bytevector-length request)  ; much smaller than equivalent JSON\n```"}]}
-{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Quickstart in Jerboa Scheme."}, {"role": "assistant", "content": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs"}]}
+{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Quickstart in Jerboa Scheme."}, {"role": "assistant", "content": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 3. Hello World in Jerboa Scheme."}, {"role": "assistant", "content": "Create `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 4. Use the REPL in Jerboa Scheme."}, {"role": "assistant", "content": "```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all."}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 5. Define a Struct in Jerboa Scheme."}, {"role": "assistant", "content": "```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```"}]}
@@ -37634,7 +37634,7 @@
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain What Clojure has that Jerboa's pmap is missing in Jerboa Scheme."}, {"role": "assistant", "content": "**1. Transients — the big one.**\nClojure lets you \"thaw\" a persistent map into a transient, do a bulk of\nmutations (`assoc!`, `dissoc!`) with near-mutable cost because only one\nowner is assumed, then freeze it back (`persistent!`). Building a 100k-entry\nmap from an alist is ~4–8× faster with transients than with repeated\n`assoc`. Jerboa's pmap has no transient mechanism — every `imap-set` inside\n`persistent-map-map` / `persistent-map-filter` / `persistent-map-merge`\nallocates a full path copy. See pmap.sls:344-358: `persistent-map-map`\nliterally does `(set! result (persistent-map-set result k (proc k v)))` in\na loop, one allocation-heavy step at a time.\n\n**2. No ArrayNode (dense-node optimization).**\nClojure has *two* interior-node types: `BitmapIndexedNode` for sparse nodes\nand `ArrayNode` for dense ones. When a bitmap-indexed node would reach ~16\nchildren, Clojure promotes it to a 32-slot flat array with `nil` in empty\nslots. Lookups in an ArrayNode skip the popcount indirection — you go\nstraight to `array[hash_chunk]`. Jerboa only has the bitmap variant, so\nevery lookup pays the popcount cost at every level, even in hot dense\nregions of the tree.\n\n**3. Leaves stored inline, not boxed.**\nClojure's `BitmapIndexedNode` stores `[k1, v1, k2, v2, …]` (or\n`[nil, child]` for nested nodes) directly in the node's single array.\nJerboa allocates a separate `hamt-leaf` record for every leaf and stores a\npointer to it in the parent's array (pmap.sls:51-52, used throughout). For\na 1M-entry map that's 1M extra heap objects and one extra pointer\nindirection on every successful lookup.\n\n**4. A hash function picked for HAMT.**\nClojure uses Murmur3 (and `mix-collection-hash`) specifically because HAMT\nperformance is very sensitive to the uniformity of the low bits. Jerboa\npmap uses Chez's stock `equal-hash`, which is a reasonable general-purpose\nhash but wasn't designed for this. In practice it's probably fine; in\npathological distributions it could mean deeper trees.\n\n**5. Structural equality and hashing.**\nA Clojure map *is* hashable by contents, so you can use a map as a key in\nanother map, and `(= m1 m2)` compares contents. Jerboa's `%pmap` is a plain\nrecord — it uses identity equality by default, so you can't use an `imap`\nas a key in an `imap` and `equal?` on two pmaps won't do what you want.\n\n**6. Seq abstraction / lazy iteration.**\nClojure maps participate in the `seq` / `reduce` / `transducer` abstractions\nfor free, including early termination via `reduced`. Jerboa provides\n`persistent-map-for-each`, `->list`, `-fold`, etc., but no lazy iterator —\nno way to stop iteration early without hand-rolling an escape continuation.\n\n**7. The `assoc` bug lives in the collision-bucket path.**\npmap.sls:122:\n\n```scheme\n[(hamt-coll? node)\n (and (= (hamt-coll-hash node) key-hash)\n      (let ([p (assoc key (hamt-coll-pairs node) equal-proc)])  ; BUG\n        p))]\n```\n\nChez's `assoc` takes 2 args, not 3. SRFI-1's `assoc` takes an optional\n3rd `=` argument, but Chez's built-in does not. So on any true hash\ncollision where you try to look up the colliding key, this will raise a\nwrong-number-of-arguments error at runtime. The insert path at\npmap.sls:209 has the same bug. Fix is straightforward:\n\n```scheme\n;; replace (assoc key pairs equal-proc) with a hand-rolled find:\n(let loop ([ps pairs])\n  (cond\n    [(null? ps) #f]\n    [(equal-proc (caar ps) key) (car ps)]\n    [else (loop (cdr ps))]))\n```\n\nThis only bites on true hash collisions, which is why the compile warning\ndoesn't show up as an obvious runtime failure in normal testing. But once\nyou fix it, collision handling becomes correct."}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Practical takeaway in Jerboa Scheme."}, {"role": "assistant", "content": "For a \"thread-safe hash-table-ish thing\" with light-to-moderate write rates,\nJerboa's pmap is functionally equivalent to Clojure's — same asymptotic\nbehavior, same thread-safety-by-immutability guarantee. The practical\ndifferences only start to bite at:\n\n- **Bulk construction** from large collections → you'll feel the lack of\n  transients (a 10× hit on `persistent-map-map` / `persistent-map-merge`\n  over millions of entries).\n- **Very large dense maps** → ArrayNode absence adds a constant factor to\n  every lookup, maybe 1.5–2× slower than Clojure at 1M+ entries.\n- **Collision-heavy workloads** → the `assoc` bug at pmap.sls:122 will\n  surface as a runtime crash. Fix it before shipping.\n\nFor the common case (thousands to tens of thousands of entries, moderate\nmutation rate, read-heavy), the gap between Jerboa's pmap and Clojure's PHM\nis not going to be your bottleneck. `imap + shared` remains the right\ndefault for a thread-safe hash-table-like value in Jerboa."}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Tutorial: Build a URL Shortener in Jerboa Scheme."}, {"role": "assistant", "content": "This tutorial walks you from `hello world` to a deployable single-file\nJerboa service. By the end you will have built **Stubby**, a small URL\nshortener with:\n\n- A `defstruct`-modeled core\n- An in-memory cache and a SQLite persistent store\n- An HTTP API on top of `(std net httpd)`\n- A test suite run by `jerboa test`\n- A packaged, redistributable single-file script\n\nEvery code block in this document is a complete runnable or compilable\nJerboa program — the project's CI verifies that.\n\n---"}]}
-{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 0. Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---"}]}
+{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 0. Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 1. Hello, world in Jerboa Scheme."}, {"role": "assistant", "content": "Create `stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Stubby is starting up.\")\n```\n\nRun it:\n\n```bash\njerboa run stubby.ss\n```\n\nOne import, one line of code. `(jerboa prelude)` is the entire\nlanguage — no separate core/text/number modules to juggle. Everything\nyou'll use below (structs, hash tables, regex, JSON, format, match)\nlives in the prelude.\n\n---"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 2. Model the domain with `defstruct` in Jerboa Scheme."}, {"role": "assistant", "content": "A short link maps a **slug** (`abc123`) to a **target URL** and tracks\nhow many times it has been resolved.\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target)\n  (make-short-link slug target 0))\n\n(def (bump-hits! link)\n  (short-link-hits-set! link (+ (short-link-hits link) 1)))\n\n(let ([ln (new-link \"abc\" \"https://example.com\")])\n  (bump-hits! ln)\n  (bump-hits! ln)\n  (printf \"slug=~a target=~a hits=~a~n\"\n          (short-link-slug ln)\n          (short-link-target ln)\n          (short-link-hits ln)))\n```\n\n`defstruct` autogenerates `make-short-link`, `short-link?`, one\naccessor per field (`short-link-slug`), and mutators with the\n`-set!` suffix (`short-link-hits-set!`). No more boilerplate.\n\n---"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 3. Generate slugs in Jerboa Scheme."}, {"role": "assistant", "content": "Real shorteners let users pass a custom slug, but fall back to a\nrandom one. We'll use the prelude's `random` and `string-append`:\n\n```scheme\n(import (jerboa prelude))\n\n(def *alphabet*\n  (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(displayln (random-slug))\n(displayln (random-slug 8))\n```\n\nNotes:\n- The ambiguous characters `0 o l 1` are omitted so slugs stay\n  copy-paste-safe over voice/print.\n- `(random N)` returns an integer in `[0, N)`.\n- `(iota n)` is `0..n-1` — the Jerboa prelude shadows Chez's\n  stricter version to give you the SRFI-1 interface.\n\n---"}]}
diff --git a/training_data_together.v1.jsonl b/training_data_together.v1.jsonl
index 64e2bf1..093e1b1 100644
--- a/training_data_together.v1.jsonl
+++ b/training_data_together.v1.jsonl
@@ -35956,7 +35956,7 @@
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Tier 4 — Niche, deferred, or non-goal in Jerboa Scheme."}, {"role": "assistant", "content": "| Feature | Section | Status | Reasoning |\n|---------|---------|--------|-----------|\n| **Clojure reader literals (`{}`/`#{}`/`[]`/`:kw`)** | [`clojure-reader.md`](./clojure-reader.md), [§4.9](./clojure-remaining.md#49-reader-literals-----v-kw) | Deferred (risky) | Requires opt-in file-local reader modes; permanent debuggability weirdness; most porters adapt to constructor forms in a day. Could be deferred forever. |\n| **Parked `go` via CPS transform** | [§10](#10-coreasync-csp-channels), §3.8 in `clojure-remaining.md` | Deferred | Research-scale macro project. Jerboa's OS-thread `go` is fine for thousands-of-coroutines use cases, just not millions. |\n| **Clojure Spec / full schema system** | [§17](#17-clojure-spec) | Non-goal | Too large to port faithfully. A scaled-down malli-style schema library is the recommended alternative for anyone who needs it. |\n| **core.logic (miniKanren)** | [§29](#29-corelogic-minikanren) | Open, low priority | A self-contained library port; valuable for anyone doing logic programming, but far from the Clojure-compat critical path. |\n| **Keywords as functions `(: k m)` on pmaps** | [§22](#22-keywordsymbol-semantics) | Deferred | Would require applicable-struct support. Instead use `(get m k)`, which is already polymorphic. |\n| **nREPL-style editor protocol** | [§37](#37-repl-driven-development-culture) | Partial | Jerboa's MCP tooling covers a lot of the same ground (`jerboa_eval`, `jerboa_repl_session`). A proper nREPL server would unlock CIDER/Calva-style editor integration but is a separate project. |\n| **Reader conditionals** | [§20](#20-reader-conditionals) | Deferred | Useful only when Jerboa has multiple targets; currently one target. Revisit if Jerboa-wasm or Gerbil-share becomes real. |\n| **`.clj` source loading** | — | Non-goal | Not porting the JVM compiler. |"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Recommendation in Jerboa Scheme."}, {"role": "assistant", "content": "Tier 1 is done. The next natural pass is **Tier 2 items in isolation** — lazy sequences, zippers, and Specter-style paths each stand alone and become more attractive now that the core ergonomics gap is closed. Tier 3's STM is the only remaining \"foundational\" gap and deserves a real design round of its own.\n\nThe language features above are what you'd port. The culture is built alongside, one idiomatic library at a time."}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Compiling Gerbil Projects with Jerboa in Jerboa Scheme."}, {"role": "assistant", "content": "This guide walks through the complete process of taking an existing Gerbil\nScheme project and compiling it to run on Chez Scheme via Jerboa."}]}
-{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://github.com/ober/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```"}]}
+{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "1. **Chez Scheme 10.x** with threads (`./configure --threads && make && make install`)\n2. **Jerboa** built and ready:\n   ```bash\n   git clone https://git.sr.ht/~lisp/jerboa ~/src/jerboa\n   cd ~/src/jerboa && make\n   ```\n3. **Gerbil source tree** (for the modules your project imports):\n   ```bash\n   # Only needed if your project imports :std/* modules\n   git clone https://github.com/mighty-gerbils/gerbil ~/src/gerbil\n   ```"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Project Layout in Jerboa Scheme."}, {"role": "assistant", "content": "A jerboa project wraps an existing Gerbil project with build scripts and a\ncompatibility layer:\n\n```\nmy-jerboa-project/\n  my-gerbil-project/        # Gerbil source (submodule or copy)\n    gerbil.pkg\n    *.ss                     # Gerbil source files (never modified)\n  src/\n    compat/                  # Chez compatibility shims (handwritten)\n      sugar.sls              # :std/sugar equivalents\n      format.sls             # :std/format equivalents\n      ...\n    mylib/                   # Generated .sls files (gitignored)\n  build-jerboa.ss            # Jerboa translation driver\n  build-all.ss               # Chez compilation trigger\n  build-binary.ss            # Optional: standalone binary builder\n  main.ss                    # Entry point program\n  Makefile\n```"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Step 1: Create the Makefile in Jerboa Scheme."}, {"role": "assistant", "content": "```makefile\nSCHEME = scheme\nJERBOA = $(or $(JERBOA_DIR),$(HOME)/src/jerboa/src)\nLIBDIRS = src:$(JERBOA)\nCOMPILE = $(SCHEME) -q --libdirs $(LIBDIRS) --compile-imported-libraries\n\n.PHONY: all jerboa compile binary run clean\n\nall: jerboa compile"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Run interpreted in Jerboa Scheme."}, {"role": "assistant", "content": "run: all\n\t$(SCHEME) -q --libdirs $(LIBDIRS) --program main.ss\n\nclean:\n\tfind src -name '*.so' -o -name '*.wpo' | xargs rm -f 2>/dev/null || true\n\trm -f src/mylib/*.sls\n```\n\nThe `JERBOA` variable points to jerboa's `src/` directory. Override it:\n```bash\nJERBOA_DIR=/path/to/jerboa/src make\n```\n\nIf your project depends on other jerboa-compiled libraries, add them to\n`LIBDIRS`:\n```makefile\nJERBOA_AWS = $(or $(JERBOA_AWS_DIR),jerboa-aws/src)\nLIBDIRS = src:$(JERBOA):$(JERBOA_AWS)\n```"}]}
@@ -36646,7 +36646,7 @@
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Clojure Equivalent in Jerboa Scheme."}, {"role": "assistant", "content": "To build the same webhook processor in Clojure, you need:\n\n```\nproject.clj (or deps.edn)          — dependency management\n├── JVM 17+                        — 200MB runtime\n├── ring/ring-core                 — HTTP abstraction\n├── http-kit or aleph              — async HTTP server\n├── compojure or reitit            — routing\n├── cheshire                       — JSON parsing\n├── core.async                     — channels\n├── mount or component             — lifecycle\n├── redis (Carmine)                — job queue\n├── sidekiq or machinery           — background workers\n├── timbre                         — logging\n└── Docker + docker-compose.yml    — deployment\n```\n\n**Result:**\n- 12+ dependencies (each with transitive deps → 100+ JARs)\n- 3 processes minimum (JVM app + Redis + worker process)\n- 30-second cold start (JVM + dependency loading)\n- 512MB-2GB RAM (JVM heap + Redis)\n- Docker image: 400MB-1GB\n- `docker-compose up` to run locally\n- 5-10 files of boilerplate before writing business logic"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Equivalent in Jerboa Scheme."}, {"role": "assistant", "content": "```\nwebhook-service.ss                  — the entire service\n```\n\n**Result:**\n- 0 external dependencies (everything is stdlib)\n- 1 process (one binary does HTTP + workers + state + WebSocket)\n- Instant startup (Chez native code, no interpreter warmup)\n- 20-40MB RAM (no GC pressure from boxed JVM objects)\n- Static binary: 15MB\n- `scp edge user@server: && ssh server ./edge` to deploy\n- ~380 lines, one file, zero config files"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Side-by-Side: Processing a Webhook in Jerboa Scheme."}, {"role": "assistant", "content": "**Clojure:**\n```clojure\n;; deps: ring, compojure, cheshire, core.async, carmine (redis)\n\n(ns webhook.handler\n  (:require [compojure.core :refer [defroutes POST]]\n            [ring.middleware.json :refer [wrap-json-body]]\n            [cheshire.core :as json]\n            [clojure.core.async :as async]\n            [taoensso.carmine :as redis]))\n\n(defroutes app-routes\n  (POST \"/hooks/:type\" [type :as req]\n    (let [payload (:body req)]\n      ;; Push to Redis queue (external process!)\n      (redis/wcar {} (redis/lpush \"webhooks\" (json/encode payload)))\n      {:status 202\n       :body (json/encode {:status \"queued\"})})))\n\n;; Separate worker process reads from Redis:\n(defn worker []\n  (loop []\n    (let [raw (redis/wcar {} (redis/brpop \"webhooks\" 0))\n          event (json/decode (second raw) true)]\n      (process-event! event)\n      (recur))))\n```\n\n**Jerboa:**\n```scheme\n;; deps: none (everything is stdlib)\n\n(import (jerboa prelude)\n        (std clojure)\n        (std net fiber-httpd)\n        (std csp clj)\n        (std text json))\n\n(def work-ch (chan 4096))\n\n(def (handle-webhook req)\n  (let ([payload (string->json-object (request-body req))])\n    (>!! work-ch payload)\n    (respond-json 202 \"{\\\"status\\\":\\\"queued\\\"}\")))\n\n;; Workers are fibers on the same process — no Redis, no IPC\n(dotimes (i 4)\n  (go (let loop ()\n        (let ([event (<!! work-ch)])\n          (when event\n            (process-event! event)\n            (loop))))))\n\n(fiber-httpd-start 8080 handle-webhook)\n```\n\nThe Jerboa version is **12 lines** vs Clojure's **20+ lines + Redis + a separate\nworker process**.  And the Jerboa version handles more concurrent connections\n(fibers vs threads), has built-in fault tolerance (supervisor), and deploys as\none file.\n\n---"}]}
-{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** [`jerboa-edge/edge.ss`](https://github.com/ober/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://github.com/ober/jerboa-edge\ncd jerboa-edge\nmake run"}]}
+{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 1: Core Demo (the ~380-line file) — COMPLETE in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** [`jerboa-edge/edge.ss`](https://git.sr.ht/~lisp/jerboa-edge)\n\nA single file that a developer can clone and run:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa-edge\ncd jerboa-edge\nmake run"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain In another terminal: in Jerboa Scheme."}, {"role": "assistant", "content": "curl -X POST localhost:8080/hooks/payment.completed \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"evt_123\",\"amount\":4999}'\n\ncurl localhost:8080/api/stats"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain => {\"total\":1,\"ok\":1,\"error\":0} in Jerboa Scheme."}, {"role": "assistant", "content": "```\n\n**Scope:**\n\n- [x] 1.1 **HTTP server with routing** — fiber-httpd with `:param` pattern routing.\n    Five endpoints: `POST /hooks/:type`, `GET /api/events/:id`, `GET /api/stats`,\n    `GET /health`, `GET /dashboard`.\n\n- [x] 1.2 **Channel-based ingestion** — CSP channel with transducer pipeline\n    (validate + dedup-by-ID + normalize).  Decouples accept from process.\n\n- [x] 1.3 **Actor-supervised workers** — N worker actors under a one-for-one\n    supervisor.  Each pulls from the channel, processes, writes to state.\n    `POST /hooks/test.crash` deliberately crashes a worker to show supervisor restart.\n\n- [x] 1.4 **STM state store** — All state in `(make-ref (make-persistent-map))`.\n    Atomic updates via `dosync`/`alter` with `(std pmap)` persistent maps.\n    Immutable snapshots — safe for concurrent reads during writes.\n\n- [x] 1.5 **WebSocket dashboard** — Single WS endpoint that streams\n    event-processed notifications to connected clients.  Sends initial stats\n    on connect.\n\n- [x] 1.6 **HMAC signature verification** — Validate `X-Signature` header using\n    `(std crypto native)` HMAC-SHA256.  Constant-time comparison.\n    Configurable via `EDGE_SECRET` env var.\n\n**Non-goals for Phase 1:**\n- Persistence across restarts (in-memory only)\n- TLS termination (use a reverse proxy, or wait for Phase 3)\n- Static binary packaging (Phase 3)\n\n**Success criteria:**\n- [x] Runs with `make run` (382 lines of Scheme)\n- [x] Handles 10K concurrent webhook POSTs without dropped events (verified: 9,993/10,000 accepted at 200 concurrent connections, ~454 req/sec)\n- [x] Worker crash + supervisor restart visible in logs\n- [x] WebSocket dashboard receives live event stream (verified with websocket-client)\n- [x] `curl` examples work out of the box (17/17 smoke tests pass)\n- [x] HMAC: no-sig and wrong-sig return 401, valid-sig returns 202"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Phase 2: Programmability in Jerboa Scheme."}, {"role": "assistant", "content": "**Deliverable:** Sandboxed user filter support + handler registry\n\n- [x] 2.1 **Sandboxed eval** — Users submit filter expressions via\n    `POST /api/filters`.  Filters evaluated per-event via\n    `restricted-eval-string` (allowlist-only: no I/O, no FFI) with a\n    2-second engine-based timeout (no fork — fiber-safe).  Events that\n    fail any filter are dropped silently before processing.\n\n    ```\n    POST /api/filters  {\"name\":\"payments-only\",\"code\":\"(equal? (cdr (assoc \\\"type\\\" evt)) \\\"payment.completed\\\")\"}\n    GET  /api/filters\n    DELETE /api/filters/:name\n    ```\n\n    Key implementation detail: event data is bound as `evt` (a deep\n    alist), so filter code uses `assoc` with string keys (not `assq`).\n\n- [x] 2.2 **Hot handler registration** — New webhook types registered at\n    runtime via `POST /api/handlers`.  No restart required.  Handler\n    code runs in a custom restricted environment (`safe-bindings` +\n    `displayln`) with a 5-second engine timeout.  Exceptions propagate\n    to the worker's retry machinery.\n\n    ```\n    POST /api/handlers  {\"type\":\"order.shipped\",\"code\":\"(displayln \\\"shipped: \\\" (cdr (assoc \\\"id\\\" evt)))\"}\n    GET  /api/handlers\n    ```\n\n- [x] 2.3 **Retry with exponential backoff** — Failed events re-enter the\n    channel with an incremented retry count.  Workers delay via\n    `fork-thread` + `sleep` (non-blocking, OS-thread safe).  Dedup\n    transducer passes retried events (`retry-count > 0`).  After 3\n    retries, events go to a dead-letter STM store.\n\n    ```\n    GET  /api/dead-letter\n    POST /api/dead-letter/:id/replay\n    ```\n\n    Replay sets `retry-count: 1` to bypass the dedup filter.\n\n**Success criteria:**\n- [x] Filter code cannot call `open-file`, `socket`, or any I/O (allowlist env)\n- [x] Filter with 2s timeout verified: `with-timeout` uses Chez engines, no fork\n- [x] Hot-registered handlers process events without service restart (verified: `order.shipped`, `inventory.updated`)\n- [x] Failed events retry 3× with 1s/2s/4s backoff then dead-letter (verified in log)\n- [x] Dead-letter replay re-processes event and clears queue (verified: status → \"ok\")\n- [x] Runs with `make run` (690 lines of Scheme)"}]}
@@ -36947,7 +36947,7 @@
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Streaming with ports in Jerboa Scheme."}, {"role": "assistant", "content": "```scheme\n(import (std text msgpack))\n\n;; Write multiple values to a port\n(let-values ([(port extract) (open-bytevector-output-port)])\n  (msgpack-pack-port 1 port)\n  (msgpack-pack-port \"hello\" port)\n  (msgpack-pack-port (vector 10 20) port)\n  (let* ([bv (extract)]\n         [in (open-bytevector-input-port bv)])\n    (list\n      (msgpack-unpack-port in)    ;=> 1\n      (msgpack-unpack-port in)    ;=> \"hello\"\n      (msgpack-unpack-port in)))) ;=> #(10 20)\n;=> (1 \"hello\" #(10 20))\n```"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Integer encoding compactness in Jerboa Scheme."}, {"role": "assistant", "content": "The encoder automatically picks the smallest representation:\n\n```scheme\n(import (std text msgpack))\n\n;; Positive fixint (1 byte for 0-127)\n(bytevector-length (msgpack-pack 0))    ;=> 1\n(bytevector-length (msgpack-pack 127))  ;=> 1\n\n;; Negative fixint (1 byte for -32 to -1)\n(bytevector-length (msgpack-pack -1))   ;=> 1\n(bytevector-length (msgpack-pack -32))  ;=> 1\n\n;; uint8 (2 bytes for 128-255)\n(bytevector-length (msgpack-pack 200))  ;=> 2\n\n;; uint16 (3 bytes for 256-65535)\n(bytevector-length (msgpack-pack 1000)) ;=> 3\n```"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Interoperability in Jerboa Scheme."}, {"role": "assistant", "content": "MessagePack is a cross-language format. Bytevectors produced by `msgpack-pack` are compatible with any MessagePack implementation (Python, Go, Rust, JavaScript, etc.) and vice versa.\n\n```scheme\n(import (std text msgpack))\n\n;; Encode a structure for sending over the network\n(define request\n  (msgpack-pack\n    '((\"method\" . \"getUser\")\n      (\"params\" . #(42))\n      (\"id\" . 1))))\n\n;; request is a compact bytevector ready for transmission\n(bytevector-length request)  ; much smaller than equivalent JSON\n```"}]}
-{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Quickstart in Jerboa Scheme."}, {"role": "assistant", "content": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs"}]}
+{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Quickstart in Jerboa Scheme."}, {"role": "assistant", "content": "# Jerboa Quickstart\n\nGet from zero to a working Jerboa program in 5 minutes.\n\n## 1. Install Chez Scheme\n\n```bash\n# Ubuntu/Debian\nsudo apt install chezscheme\n\n# macOS\nbrew install chezscheme\n\n# From source: https://cisco.github.io/ChezScheme/\n```\n\nVerify: `scheme --version` should print 10.x or later.\n\n## 2. Get Jerboa\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\n```\n\n## 3. Hello World\n\nCreate `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```\n\n## 4. Use the REPL\n\n```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all.\n\n## 5. Define a Struct\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```\n\n## 6. Pattern Matching\n\n```scheme\n(import (jerboa prelude))\n\n(def (describe val)\n  (match val\n    ((list a b c) (format \"three-element list: ~a ~a ~a\" a b c))\n    ((cons h t)   (format \"pair: ~a . ~a\" h t))\n    ((? string?)  (format \"string: ~a\" val))\n    ((? number?)  (format \"number: ~a\" val))\n    (_            \"something else\")))\n\n(displayln (describe '(1 2 3)))    ;; three-element list: 1 2 3\n(displayln (describe \"hello\"))     ;; string: hello\n(displayln (describe 42))          ;; number: 42\n```\n\nFor struct pattern matching, use `(std match2)`:\n\n```scheme\n(import (jerboa prelude) (std match2))\n\n(defstruct point (x y))\n(define-match-type point point? point-x point-y)\n\n(def (describe-point p)\n  (match p\n    [(point x y) (format \"(~a, ~a)\" x y)]))\n```\n\n## 7. Parse JSON\n\n```scheme\n(import (jerboa prelude))\n\n(let ([data (string->json-object \"{\\\"name\\\": \\\"Jerboa\\\", \\\"version\\\": 1}\")])\n  (displayln (hash-ref data \"name\"))     ;; Jerboa\n  (displayln (hash-ref data \"version\"))) ;; 1\n\n;; Write JSON\n(displayln (json-object->string\n  (list->hash-table '((\"language\" . \"Jerboa\") (\"fast\" . #t)))))\n```\n\n## 8. HTTP Server\n\n```scheme\n(import (jerboa prelude)\n        (std net httpd)\n        (std net router)\n        (std text json))\n\n(define routes\n  (make-router\n    (route \"GET\" \"/\" (lambda (req) '((status . 200) (body . \"Hello!\"))))\n    (route \"GET\" \"/api/status\"\n      (lambda (req)\n        `((status . 200)\n          (headers . ((\"Content-Type\" . \"application/json\")))\n          (body . ,(json-object->string\n                     (list->hash-table '((\"status\" . \"ok\"))))))))))\n\n;; Start server on port 8080\n(start-httpd 8080 routes)\n```\n\n## 9. Run in a Security Sandbox\n\n```scheme\n(import (jerboa prelude safe))  ;; safe prelude: contracts + sandboxing\n\n;; Run untrusted code with restrictions:\n;; - Landlock filesystem sandboxing\n;; - seccomp syscall filtering\n;; - Engine-based timeout\n(run-safe\n  (lambda ()\n    (displayln \"I'm sandboxed!\"))\n  #:timeout 5\n  #:allow-read '(\"/tmp\"))\n```\n\n## 10. Run Tests\n\nCreate `tests/test-mylib.ss`:\n\n```scheme\n(import (chezscheme) (jerboa prelude) (std test))\n\n(test-suite \"my library\"\n  (test-case \"addition works\"\n    (check (+ 1 2) => 3))\n  (test-case \"strings\"\n    (check (string-join '(\"a\" \"b\") \",\") => \"a,b\")))\n\n(run-tests!)\n```\n\nRun:\n\n```bash\nbin/jerboa test tests/\n```\n\n## Next Steps\n\n- Browse the [module inventory](../README.md) for available libraries\n- Read the [security guide](safety-guide.md) for sandboxing details\n- See the [type system docs](typing.md) for gradual typing\n- Check [import-conflicts.md](import-conflicts.md) when porting from Gerbil\n- Explore [examples/](../examples/) for complete programs"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 3. Hello World in Jerboa Scheme."}, {"role": "assistant", "content": "Create `hello.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Hello from Jerboa!\")\n```\n\nRun it:\n\n```bash\nbin/jerboa run hello.ss\n```\n\nOr without the CLI wrapper:\n\n```bash\nscheme --libdirs lib --script hello.ss\n```"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 4. Use the REPL in Jerboa Scheme."}, {"role": "assistant", "content": "```bash\nbin/jerboa\n```\n\n```\njerboa> (+ 1 2)\n3\njerboa> (sort '(5 1 3 2 4) <)\n(1 2 3 4 5)\njerboa> ,help\n```\n\nThe REPL supports value history (`*`, `**`, `***`, `$1`, `$2`...),\ninspection (`,type`, `,describe`), profiling (`,time`, `,bench`), and\n40+ other commands. Type `,help` to see them all."}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 5. Define a Struct in Jerboa Scheme."}, {"role": "assistant", "content": "```scheme\n(import (jerboa prelude))\n\n(defstruct point (x y))\n\n(def (distance p)\n  (let ([x (point-x p)]\n        [y (point-y p)])\n    (sqrt (+ (* x x) (* y y)))))\n\n(let ([p (make-point 3 4)])\n  (printf \"Point: (~a, ~a)\\n\" (point-x p) (point-y p))\n  (printf \"Distance: ~a\\n\" (distance p)))\n```"}]}
@@ -37328,7 +37328,7 @@
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain What Clojure has that Jerboa's pmap is missing in Jerboa Scheme."}, {"role": "assistant", "content": "**1. Transients — the big one.**\nClojure lets you \"thaw\" a persistent map into a transient, do a bulk of\nmutations (`assoc!`, `dissoc!`) with near-mutable cost because only one\nowner is assumed, then freeze it back (`persistent!`). Building a 100k-entry\nmap from an alist is ~4–8× faster with transients than with repeated\n`assoc`. Jerboa's pmap has no transient mechanism — every `imap-set` inside\n`persistent-map-map` / `persistent-map-filter` / `persistent-map-merge`\nallocates a full path copy. See pmap.sls:344-358: `persistent-map-map`\nliterally does `(set! result (persistent-map-set result k (proc k v)))` in\na loop, one allocation-heavy step at a time.\n\n**2. No ArrayNode (dense-node optimization).**\nClojure has *two* interior-node types: `BitmapIndexedNode` for sparse nodes\nand `ArrayNode` for dense ones. When a bitmap-indexed node would reach ~16\nchildren, Clojure promotes it to a 32-slot flat array with `nil` in empty\nslots. Lookups in an ArrayNode skip the popcount indirection — you go\nstraight to `array[hash_chunk]`. Jerboa only has the bitmap variant, so\nevery lookup pays the popcount cost at every level, even in hot dense\nregions of the tree.\n\n**3. Leaves stored inline, not boxed.**\nClojure's `BitmapIndexedNode` stores `[k1, v1, k2, v2, …]` (or\n`[nil, child]` for nested nodes) directly in the node's single array.\nJerboa allocates a separate `hamt-leaf` record for every leaf and stores a\npointer to it in the parent's array (pmap.sls:51-52, used throughout). For\na 1M-entry map that's 1M extra heap objects and one extra pointer\nindirection on every successful lookup.\n\n**4. A hash function picked for HAMT.**\nClojure uses Murmur3 (and `mix-collection-hash`) specifically because HAMT\nperformance is very sensitive to the uniformity of the low bits. Jerboa\npmap uses Chez's stock `equal-hash`, which is a reasonable general-purpose\nhash but wasn't designed for this. In practice it's probably fine; in\npathological distributions it could mean deeper trees.\n\n**5. Structural equality and hashing.**\nA Clojure map *is* hashable by contents, so you can use a map as a key in\nanother map, and `(= m1 m2)` compares contents. Jerboa's `%pmap` is a plain\nrecord — it uses identity equality by default, so you can't use an `imap`\nas a key in an `imap` and `equal?` on two pmaps won't do what you want.\n\n**6. Seq abstraction / lazy iteration.**\nClojure maps participate in the `seq` / `reduce` / `transducer` abstractions\nfor free, including early termination via `reduced`. Jerboa provides\n`persistent-map-for-each`, `->list`, `-fold`, etc., but no lazy iterator —\nno way to stop iteration early without hand-rolling an escape continuation.\n\n**7. The `assoc` bug lives in the collision-bucket path.**\npmap.sls:122:\n\n```scheme\n[(hamt-coll? node)\n (and (= (hamt-coll-hash node) key-hash)\n      (let ([p (assoc key (hamt-coll-pairs node) equal-proc)])  ; BUG\n        p))]\n```\n\nChez's `assoc` takes 2 args, not 3. SRFI-1's `assoc` takes an optional\n3rd `=` argument, but Chez's built-in does not. So on any true hash\ncollision where you try to look up the colliding key, this will raise a\nwrong-number-of-arguments error at runtime. The insert path at\npmap.sls:209 has the same bug. Fix is straightforward:\n\n```scheme\n;; replace (assoc key pairs equal-proc) with a hand-rolled find:\n(let loop ([ps pairs])\n  (cond\n    [(null? ps) #f]\n    [(equal-proc (caar ps) key) (car ps)]\n    [else (loop (cdr ps))]))\n```\n\nThis only bites on true hash collisions, which is why the compile warning\ndoesn't show up as an obvious runtime failure in normal testing. But once\nyou fix it, collision handling becomes correct."}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Practical takeaway in Jerboa Scheme."}, {"role": "assistant", "content": "For a \"thread-safe hash-table-ish thing\" with light-to-moderate write rates,\nJerboa's pmap is functionally equivalent to Clojure's — same asymptotic\nbehavior, same thread-safety-by-immutability guarantee. The practical\ndifferences only start to bite at:\n\n- **Bulk construction** from large collections → you'll feel the lack of\n  transients (a 10× hit on `persistent-map-map` / `persistent-map-merge`\n  over millions of entries).\n- **Very large dense maps** → ArrayNode absence adds a constant factor to\n  every lookup, maybe 1.5–2× slower than Clojure at 1M+ entries.\n- **Collision-heavy workloads** → the `assoc` bug at pmap.sls:122 will\n  surface as a runtime crash. Fix it before shipping.\n\nFor the common case (thousands to tens of thousands of entries, moderate\nmutation rate, read-heavy), the gap between Jerboa's pmap and Clojure's PHM\nis not going to be your bottleneck. `imap + shared` remains the right\ndefault for a thread-safe hash-table-like value in Jerboa."}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain Jerboa Tutorial: Build a URL Shortener in Jerboa Scheme."}, {"role": "assistant", "content": "This tutorial walks you from `hello world` to a deployable single-file\nJerboa service. By the end you will have built **Stubby**, a small URL\nshortener with:\n\n- A `defstruct`-modeled core\n- An in-memory cache and a SQLite persistent store\n- An HTTP API on top of `(std net httpd)`\n- A test suite run by `jerboa test`\n- A packaged, redistributable single-file script\n\nEvery code block in this document is a complete runnable or compilable\nJerboa program — the project's CI verifies that.\n\n---"}]}
-{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 0. Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://github.com/ober/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---"}]}
+{"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 0. Prerequisites in Jerboa Scheme."}, {"role": "assistant", "content": "- Chez Scheme ≥ 10.x (`scheme --version`)\n- A clone of Jerboa with `bin/jerboa` on your `PATH`:\n\n```bash\ngit clone https://git.sr.ht/~lisp/jerboa.git\ncd jerboa\nmake build\nexport PATH=\"$PWD/bin:$PATH\"\n```\n\nVerify:\n\n```bash\njerboa version\n```\n\n---"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 1. Hello, world in Jerboa Scheme."}, {"role": "assistant", "content": "Create `stubby.ss`:\n\n```scheme\n(import (jerboa prelude))\n\n(displayln \"Stubby is starting up.\")\n```\n\nRun it:\n\n```bash\njerboa run stubby.ss\n```\n\nOne import, one line of code. `(jerboa prelude)` is the entire\nlanguage — no separate core/text/number modules to juggle. Everything\nyou'll use below (structs, hash tables, regex, JSON, format, match)\nlives in the prelude.\n\n---"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 2. Model the domain with `defstruct` in Jerboa Scheme."}, {"role": "assistant", "content": "A short link maps a **slug** (`abc123`) to a **target URL** and tracks\nhow many times it has been resolved.\n\n```scheme\n(import (jerboa prelude))\n\n(defstruct short-link (slug target hits))\n\n(def (new-link slug target)\n  (make-short-link slug target 0))\n\n(def (bump-hits! link)\n  (short-link-hits-set! link (+ (short-link-hits link) 1)))\n\n(let ([ln (new-link \"abc\" \"https://example.com\")])\n  (bump-hits! ln)\n  (bump-hits! ln)\n  (printf \"slug=~a target=~a hits=~a~n\"\n          (short-link-slug ln)\n          (short-link-target ln)\n          (short-link-hits ln)))\n```\n\n`defstruct` autogenerates `make-short-link`, `short-link?`, one\naccessor per field (`short-link-slug`), and mutators with the\n`-set!` suffix (`short-link-hits-set!`). No more boilerplate.\n\n---"}]}
 {"messages": [{"role": "system", "content": "You are an expert in Jerboa Scheme, a Chez-Scheme-based dialect with a Gerbil-flavored prelude. You provide accurate, idiomatic Jerboa code with correct imports, function names, and arities. Module paths use the (jerboa ...) and (std ...) forms — never :std/foo (Gerbil) or (srfi :NN) (R7). You know the prelude, the actor system, fibers, the FFI, capability security, the macro system (defrules, syntax-case), pattern matching (match), and how Jerboa diverges from Gerbil/Racket/Clojure/SRFI. When writing code, always include required (import ...) statements."}, {"role": "user", "content": "Explain 3. Generate slugs in Jerboa Scheme."}, {"role": "assistant", "content": "Real shorteners let users pass a custom slug, but fall back to a\nrandom one. We'll use the prelude's `random` and `string-append`:\n\n```scheme\n(import (jerboa prelude))\n\n(def *alphabet*\n  (string->list \"abcdefghijkmnpqrstuvwxyz23456789\"))\n\n(def (random-slug (n 6))\n  (list->string\n    (map (lambda (_) (list-ref *alphabet* (random (length *alphabet*))))\n         (iota n))))\n\n(displayln (random-slug))\n(displayln (random-slug 8))\n```\n\nNotes:\n- The ambiguous characters `0 o l 1` are omitted so slugs stay\n  copy-paste-safe over voice/print.\n- `(random N)` returns an integer in `[0, N)`.\n- `(iota n)` is `0..n-1` — the Jerboa prelude shadows Chez's\n  stricter version to give you the SRFI-1 interface.\n\n---"}]}