updates
ober
8c30c7299161874b397dddf7b26a5dd38c60b55b
--- a/bin/jerboa +++ b/bin/jerboa @@ -37,7 +37,7 @@ Usage: jerboa [command] [args...] Commands: (no args) Launch the Jerboa REPL repl Launch the Jerboa REPL - run <file> Run a Scheme script + run <file> [args] Run a Scheme script exec <file> Run a self-contained script with a requires: header (auto-installs declared dependencies before running) eval '<expr>' Evaluate a single expression @@ -68,15 +68,16 @@ REPL cmd_run() { if [ $# -eq 0 ]; then echo "Error: jerboa run requires a file argument" >&2 - echo "Usage: jerboa run <file>" >&2 + echo "Usage: jerboa run <file> [args...]" >&2 exit 1 fi local file="$1" + shift if [ ! -f "$file" ]; then echo "Error: file not found: $file" >&2 exit 1 fi - exec "$SCHEME" --libdirs "$LIBDIRS" --script "$file" + exec "$SCHEME" --libdirs "$LIBDIRS" --script "$file" "$@" } ## --- a/data/changelog.sexp +++ b/data/changelog.sexp @@ -74,7 +74,7 @@ ("moved") ("notes" . - "LLVM IR backend gained Option, variants, and match. (Option T) lowers to a by-value tagged struct { i1, T } (option-some/option-none). Variants lower to a tagged boxed { i32 tag, ptr box }: the constructor sizes the case's field struct (getelementptr-null + ptrtoint), mallocs the box, and stores each field (null for a fieldless case; never freed; recursive variants supported). match lowers to a switch on the tag, each arm loading its bound fields from the box and joining results through a phi, with an unreachable default when exhaustive or the else arm otherwise. Demonstrated by carving jerboa-secmon's pure psk-hex kernels (constant-time compare + lowercase hex codec) out of psk into their own module, which now compiles whole-program to native code (78 functions) and is asserted against the Rust backend. The crypto halves (sha256/hkdf/aes-gcm/x25519) remain on the RustCrypto FFI path. See docs/llvmir-backend.md.") + "LLVM IR backend gained Option, variants, and match. (Option T) lowers to a by-value tagged struct { i1, T } (option-some/option-none). Variants lower to a tagged boxed { i32 tag, ptr box }: the constructor sizes the case's field struct (getelementptr-null + ptrtoint), mallocs the box, and stores each field (null for a fieldless case; never freed; recursive variants supported). match lowers to a switch on the tag, each arm loading its bound fields from the box and joining results through a phi, with an unreachable default when exhaustive or the else arm otherwise. Demonstrated by carving jerboa-secmon's pure psk-hex kernels (constant-time compare + lowercase hex codec) out of psk into their own module, which now compiles whole-program to native code (78 functions) and is asserted against the Rust backend. The crypto halves (sha256/hkdf/aes-gcm/x25519) remain on the RustCrypto FFI path. ") ("removed") ("renamed") ("tier_changes") @@ -86,7 +86,7 @@ ("moved") ("notes" . - "LLVM IR backend expanded past the scalar MVP to cover the pure compute subset Typed Jerboa kernels actually use: String/Bytes as a by-value { ptr, i64 } fat pointer (string->utf8/utf8->string are value identities; string literals intern deduped private constants; bytevector-length/u8-ref via extractvalue/gep/load; bytes-build via malloc + store loop), by-value records ({ f0ty, ... } structs, insertvalue ctor / extractvalue accessor, usable as for/fold accumulators via a struct phi), and whole-program cross-module emission (topo-sorted, registry-checked, global name->symbol map so imported calls target @jt_llvm_<defining-module>__<def>; duplicate names across modules rejected). CLI gains --whole-program OUT.ll. Proven end-to-end on jerboa-secmon: its six pure detection kernels (analytics/strbytes/triage/lolbin/obfuscate/dga) compile whole-program to one native object (68 functions, verify + -O2) and a C harness asserts every headline kernel against the Rust backend's values. The crypto trio (sha256/hmac/hkdf/aes-gcm/x25519) is intentionally left on the RustCrypto FFI path. See docs/llvmir-backend.md.") + "LLVM IR backend expanded past the scalar MVP to cover the pure compute subset Typed Jerboa kernels actually use: String/Bytes as a by-value { ptr, i64 } fat pointer (string->utf8/utf8->string are value identities; string literals intern deduped private constants; bytevector-length/u8-ref via extractvalue/gep/load; bytes-build via malloc + store loop), by-value records ({ f0ty, ... } structs, insertvalue ctor / extractvalue accessor, usable as for/fold accumulators via a struct phi), and whole-program cross-module emission (topo-sorted, registry-checked, global name->symbol map so imported calls target @jt_llvm_<defining-module>__<def>; duplicate names across modules rejected). CLI gains --whole-program OUT.ll. Proven end-to-end on jerboa-secmon: its six pure detection kernels (analytics/strbytes/triage/lolbin/obfuscate/dga) compile whole-program to one native object (68 functions, verify + -O2) and a C harness asserts every headline kernel against the Rust backend's values. The crypto trio (sha256/hmac/hkdf/aes-gcm/x25519) is intentionally left on the RustCrypto FFI path. ") ("removed") ("renamed") ("tier_changes") @@ -101,7 +101,7 @@ ("moved") ("notes" . - "Experimental Typed Jerboa -> textual LLVM IR backend (scalar subset): Nat/Int/Bool/Float plus Unit returns, let/begin/if (phi joins), direct same-module calls, arithmetic/comparisons/equal?/bool, bitwise and shifts, exact->inexact, log2 via llvm.log2.f64, and for/fold over in-range. Deterministic .ll per typed-library form via support/typed-llvmir.ss; Makefile targets typed-llvmir, typed-llvmir-check (llvm-as + opt -passes=verify + opt -O2), typed-llvmir-smoke (llc + cc executable, expected exit status), and typed-llvmir-parity (observed-result comparison against the Rust backend). Rust remains the reference backend. See docs/llvmir-backend.md.") + "Experimental Typed Jerboa -> textual LLVM IR backend (scalar subset): Nat/Int/Bool/Float plus Unit returns, let/begin/if (phi joins), direct same-module calls, arithmetic/comparisons/equal?/bool, bitwise and shifts, exact->inexact, log2 via llvm.log2.f64, and for/fold over in-range. Deterministic .ll per typed-library form via support/typed-llvmir.ss; Makefile targets typed-llvmir, typed-llvmir-check (llvm-as + opt -passes=verify + opt -O2), typed-llvmir-smoke (llc + cc executable, expected exit status), and typed-llvmir-parity (observed-result comparison against the Rust backend). Rust remains the reference backend. ") ("removed") ("renamed") ("tier_changes") --- a/data/cookbooks.sexp +++ b/data/cookbooks.sexp @@ -4,7 +4,7 @@ ("imports" "(jerboa prelude)" "(std misc thread)") ("notes" . - "jerbuild binary <entry.ss> <out> compiles via compile-program, which (unlike scheme --script) supplies no implicit base language. (jerboa prelude) and (std ...) are ADDITIVE over (chezscheme), not a standalone base, so a raw full-Jerboa entry would otherwise fail to compile with unbound display, /, etc. jerbuild auto-detects an entry importing no chez base ((chezscheme)/(rnrs)/(scheme)) and wraps it, prepending (import (except (chezscheme) <conflicts>) <orig-imports>); the except-list is computed dynamically via environment-symbols (the prelude/std export names that collide with chezscheme). So you write clean Jerboa with no #!chezscheme. Needs the jerbuild multicall symlink: the C launcher sets JERBOA_MULTICALL_NAME from argv0, so JERBOA_MULTICALL_NAME=jerbuild jerboa does NOT work. On macOS, make install ad-hoc re-signs the binary because overwriting a Mach-O invalidates its signature and the kernel SIGKILLs it (Killed: 9).") + "jerbuild binary <entry.ss> <out> compiles via compile-program, which supplies no implicit base language in compile-program mode. (jerboa prelude) and (std ...) are ADDITIVE over (chezscheme), not a standalone base, so a raw full-Jerboa entry would otherwise fail to compile with unbound display, /, etc. jerbuild auto-detects an entry importing no chez base ((chezscheme)/(rnrs)/(scheme)) and wraps it, prepending (import (except (chezscheme) <conflicts>) <orig-imports>); the except-list is computed dynamically via environment-symbols (the prelude/std export names that collide with chezscheme). So you write clean Jerboa with no #!chezscheme. Needs the jerbuild multicall symlink: the C launcher sets JERBOA_MULTICALL_NAME from argv0, so JERBOA_MULTICALL_NAME=jerbuild jerboa does NOT work. On macOS, make install ad-hoc re-signs the binary because overwriting a Mach-O invalidates its signature and the kernel SIGKILLs it (Killed: 9).") ("tags" "jerbuild" "binary" "native" "executable" "compile" "prelude") ("title" @@ -80,7 +80,7 @@ ("title" . "File I/O")) (("code" . - "(import (std test))\n\n; Define a test suite\n(test-suite \"my-module tests\"\n \n (test \"basic arithmetic\"\n (check (+ 1 2) => 3)\n (check (- 10 3) => 7))\n \n (test \"string operations\"\n (check (string-length \"hello\") => 5)\n (check (string-append \"foo\" \"bar\") => \"foobar\"))\n \n (test \"list operations\"\n (check (length '(1 2 3)) => 3)\n (check (car '(1 2 3)) => 1)))\n\n; Run with: scheme --libdirs $JERBOA_HOME/lib --script test.ss") ("id" . "jerboa-test-suite") ("imports" "(std test)") + "(import (std test))\n\n; Define a test suite\n(test-suite \"my-module tests\"\n \n (test \"basic arithmetic\"\n (check (+ 1 2) => 3)\n (check (- 10 3) => 7))\n \n (test \"string operations\"\n (check (string-length \"hello\") => 5)\n (check (string-append \"foo\" \"bar\") => \"foobar\"))\n \n (test \"list operations\"\n (check (length '(1 2 3)) => 3)\n (check (car '(1 2 3)) => 1)))\n\n; Run with: jerboa run test.ss") ("id" . "jerboa-test-suite") ("imports" "(std test)") ("notes" . "Tests use (std test) module. check compares with =>, string= for strings, equal? for structures.") @@ -88,7 +88,7 @@ ("title" . "Writing Tests")) (("code" . - "#!/usr/bin/env scheme --libdirs ${JERBOA_HOME:-~/mine/jerboa}/lib --script\n\n; Or run manually:\n; scheme --libdirs $JERBOA_HOME/lib --script myscript.ss\n\n; Compile imported libraries for speed:\n; scheme --compile-imported-libraries --libdirs $JERBOA_HOME/lib --script myscript.ss\n\n(import (jerboa prelude))\n\n; Command-line args\n(def args (cdr (command-line)))\n\n(display \"Hello from Jerboa!\")\n(newline)") ("id" . "jerboa-run-script") ("imports") + "#!/usr/bin/env -S jerboa run\n\n; Or run manually:\n; jerboa run myscript.ss\n\n; Compile imported libraries for speed:\n; jerboa build\n\n(import (jerboa prelude))\n\n; Command-line args\n(def args (cdr (command-line)))\n\n(display \"Hello from Jerboa!\")\n(newline)") ("id" . "jerboa-run-script") ("imports") ("notes" . "Set JERBOA_HOME to point to your Jerboa installation. The --compile-imported-libraries flag pre-compiles .sls to .so for faster startup.") @@ -357,7 +357,7 @@ ("title" . "Format String Directives")) (("code" . - "; File: lib/my-project/utils.sls\n(library (my-project utils)\n (export helper square greet)\n (import (jerboa prelude))\n\n (def (helper x) (* x 2))\n (def (square x) (* x x))\n (def (greet name) (format \"Hello, ~a!\" name)))\n\n; File: main.ss (script)\n(import (my-project utils))\n\n(displayln (greet \"World\")) ; => Hello, World!\n(displayln (square 5)) ; => 25\n\n; Run with:\n; scheme --libdirs lib --script main.ss\n\n; Selective import in another module\n(import (only (my-project utils) square))\n(import (except (my-project utils) greet))\n(import (rename (my-project utils) (helper h)))") ("id" . "jerboa-library-definition") ("imports") + "; File: lib/my-project/utils.sls\n(library (my-project utils)\n (export helper square greet)\n (import (jerboa prelude))\n\n (def (helper x) (* x 2))\n (def (square x) (* x x))\n (def (greet name) (format \"Hello, ~a!\" name)))\n\n; File: main.ss (script)\n(import (my-project utils))\n\n(displayln (greet \"World\")) ; => Hello, World!\n(displayln (square 5)) ; => 25\n\n; Run with:\n; jerboa run main.ss\n\n; Selective import in another module\n(import (only (my-project utils) square))\n(import (except (my-project utils) greet))\n(import (rename (my-project utils) (helper h)))") ("id" . "jerboa-library-definition") ("imports") ("notes" . "Jerboa uses R6RS library system. Files use .sls extension. Module path matches directory structure: (my-project utils) -> lib/my-project/utils.sls. Must explicitly list exports (no export-all). Use --libdirs to point to the lib/ directory.") @@ -396,16 +396,16 @@ ("title" . "Read errno value from Chez Scheme FFI")) (("code" . - ";;; BUG: In --script mode, this HANGS because the forked thread\n;;; doesn't get to run before the next top-level form evaluates.\n\n;; (define server (start-tcp-server! 0)) ;; forks thread\n;; (sleep-ms 200) ;; thread still hasn't run\n;; (define conn (tcp-connect \"127.0.0.1\" server)) ;; HANGS - nobody accepts\n\n;;; FIX 1: Use --program mode instead of --script\n;; scheme --program tests/mytest.ss\n;; In --program mode, entire file is compiled as one unit; all top-level\n;; defines are in scope together and threads run properly.\n\n;;; FIX 2: Wrap in begin (acts like a single top-level expression)\n(begin\n (define server (start-tcp-server! 0))\n (sleep (make-time 'time-duration 200000000 0))\n (define-values (in out) (tcp-connect \"127.0.0.1\" server))\n (get-line in))\n\n;;; FIX 3: Use let instead of define\n(let* ((server (start-tcp-server! 0))\n (dummy (sleep (make-time 'time-duration 200000000 0))))\n (define-values (in out) (tcp-connect \"127.0.0.1\" server))\n (get-line in))") ("id" . "jerboa-script-mode-threads") + ";;; BUG: Split top-level threaded setup can hang under jerboa run.\n;;; The forked thread may not run before the next top-level form evaluates.\n\n;; (define server (start-tcp-server! 0)) ;; forks thread\n;; (sleep-ms 200) ;; thread still hasn't run\n;; (define conn (tcp-connect \"127.0.0.1\" server)) ;; HANGS - nobody accepts\n\n;;; FIX 1: Wrap in begin (acts like a single top-level expression)\n(begin\n (define server (start-tcp-server! 0))\n (sleep (make-time 'time-duration 200000000 0))\n (define-values (in out) (tcp-connect \"127.0.0.1\" server))\n (get-line in))\n\n;;; FIX 2: Use let instead of define\n(let* ((server (start-tcp-server! 0))\n (dummy (sleep (make-time 'time-duration 200000000 0))))\n (define-values (in out) (tcp-connect \"127.0.0.1\" server))\n (get-line in))") ("id" . "jerboa-script-mode-threads") ("imports" "(chezscheme)") ("notes" . - "In Chez --script mode, each top-level form is compiled and evaluated independently. A fork-thread call in one form creates a thread, but that thread doesn't get CPU time until the main thread blocks or yields — which doesn't happen between top-level form evaluations. Use --program mode for test files that fork threads at the top level. This is especially relevant for TCP server tests where you start a server with fork-thread then immediately connect.") + "Jerboa run uses Chez script evaluation internally, where each top-level form is compiled and evaluated independently. A fork-thread call in one form creates a thread, but that thread may not get CPU time before the next top-level form runs. Keep threaded setup and use in one begin/let form.") ("tags" "script" "program" "threads" "fork-thread" "top-level" "define" "bug") ("title" . - "Chez --script mode: forked threads don't run between top-level defines")) + "Jerboa run: forked threads may not run between top-level defines")) (("code" . ";;; DEADLOCK: Handler thread calls (collect), main thread is in c-read\n;;;\n;;; Thread A: (collect) → stop-the-world, waits for all threads safe\n;;; Thread B: blocked in c-read (foreign call) → can't reach Chez safe point\n;;; because c-read returns only when data arrives\n;;; but data won't arrive because Thread A is stuck\n;;;\n;;; This deadlocks if Thread B is waiting for Thread A to send data first.\n\n;;; WRONG — calling (collect) while another thread is in blocking I/O:\n;; (define (handle-command cmd out-port)\n;; (when (string=? cmd \",gc\")\n;; (collect) ; DEADLOCK if any thread is in c-read/c-accept/etc.\n;; (write-safe out-port \"GC done\\n\")))\n\n;;; RIGHT — report stats without forcing collection:\n(define (handle-gc-command out-port)\n (write-safe out-port\n (string-append \" GC done. bytes-allocated: \"\n (number->string (bytes-allocated)) \"\\n\")))\n\n;;; Alternative: use collect-notify (if available, Chez-specific)\n;;; Or just document that ,gc only reports stats, doesn't force GC") ("id" . "jerboa-collect-deadlock") @@ -420,11 +420,11 @@ "Chez (collect) deadlocks if another thread is in a blocking foreign call")) (("code" . - ";;; BUG: In Gerbil, void accepts any number of args.\n;;; In Chez Scheme, (void) takes exactly 0 arguments.\n;;;\n;;; (with-catch void thunk) calls (void e) with 1 arg → arity error!\n\n;;; WRONG (crashes in --program mode / strict Chez):\n;; (with-catch void (lambda () (delete-file \"tmp.txt\")))\n\n;;; RIGHT: Use a variadic lambda that ignores its args:\n(with-catch (lambda _ (void))\n (lambda () (delete-file \"tmp.txt\")))\n\n;;; Or a named 1-arg lambda:\n(with-catch (lambda (e) (void))\n (lambda () (delete-file \"tmp.txt\")))\n\n;;; This applies everywhere void is used as an error handler:\n(with-catch (lambda _ (void)) (lambda () (close-port p)))\n(with-catch (lambda _ (void)) (lambda () (tcp-close srv)))") ("id" . "jerboa-void-handler-arity") + ";;; BUG: In Gerbil, void accepts any number of args.\n;;; In Chez Scheme, (void) takes exactly 0 arguments.\n;;;\n;;; (with-catch void thunk) calls (void e) with 1 arg → arity error!\n\n;;; WRONG (crashes in strict compiled mode):\n;; (with-catch void (lambda () (delete-file \"tmp.txt\")))\n\n;;; RIGHT: Use a variadic lambda that ignores its args:\n(with-catch (lambda _ (void))\n (lambda () (delete-file \"tmp.txt\")))\n\n;;; Or a named 1-arg lambda:\n(with-catch (lambda (e) (void))\n (lambda () (delete-file \"tmp.txt\")))\n\n;;; This applies everywhere void is used as an error handler:\n(with-catch (lambda _ (void)) (lambda () (close-port p)))\n(with-catch (lambda _ (void)) (lambda () (tcp-close srv)))") ("id" . "jerboa-void-handler-arity") ("imports" "(chezscheme)") ("notes" . - "Gerbil's void procedure is variadic — (void 1 2 3) works. Chez's built-in void takes exactly 0 arguments. When porting code that uses (with-catch void ...) or any error handler position, replace void with (lambda _ (void)) or (lambda (e) (void)). This error may only surface in --program mode (strict mode) but not --script mode, making it easy to miss.") + "Gerbil's void procedure is variadic — (void 1 2 3) works. Chez's built-in void takes exactly 0 arguments. When porting code that uses (with-catch void ...) or any error handler position, replace void with (lambda _ (void)) or (lambda (e) (void)). This error may only surface in strict compiled mode and can be easy to miss, making it easy to miss.") ("tags" "void" "with-catch" "handler" "arity" "gerbil-compat") ("title" @@ -436,7 +436,7 @@ ("imports" "(chezscheme)" "(std net tcp)") ("notes" . - "TCP server tests that reuse a single server instance accumulate stale connections/fds and eventually hang. Limit connections per server to 2-3, or use a fresh server per test group. Also: test files using fork-thread MUST use --program mode (not --script mode), because in --script mode forked threads don't run between top-level form evaluations.") + "TCP server tests that reuse a single server instance accumulate stale connections/fds and eventually hang. Limit connections per server to 2-3, or use a fresh server per test group. Also: keep fork-thread setup and use in one top-level begin/let form under jerboa run, because split top-level forms may run before the helper thread is scheduled.") ("tags" "tcp" "test" "server" "fork-thread" "pattern" "debug-repl") ("title" @@ -1260,7 +1260,7 @@ ("imports" "(std security import-audit)") ("notes" . - "Use in CI/build pipeline to prevent AI-generated code from bypassing the capability system via direct (chezscheme) imports. Trusted infrastructure modules (lib/jerboa/, lib/std/security/, etc.) are automatically exempt. Add to Makefile: scheme --libdirs lib --script tools/audit-imports.ss") + "Use in CI/build pipeline to prevent AI-generated code from bypassing the capability system via direct (chezscheme) imports. Trusted infrastructure modules (lib/jerboa/, lib/std/security/, etc.) are automatically exempt. Add to Makefile: jerboa run tools/audit-imports.ss") ("tags" "audit" "import" "security" "build" "policy" "chezscheme") ("title" . "Audit Source Files for Forbidden Imports")) @@ -2474,11 +2474,11 @@ "PEG grammar with packrat memoization via (std peg)")) (("code" . - ";; #r\"...\" raw strings are parsed by the Jerboa reader, not Chez's default reader.\n;; In .ss files run with scheme --script, you CANNOT use #r\"...\" literals directly.\n;; To test raw string parsing, call jerboa-read-string on a source string.\n\n;; KEY: without a path argument, jerboa-read-string returns PLAIN values (not annotated datums)\n(define (read1 str)\n (car (jerboa-read-string str))) ; <-- just car, NOT annotated-datum-value\n\n(read1 \"#r\\\"\\\\d+\\\"\") ; => \"\\\\d+\" (the 3-char string backslash,d,plus)\n(read1 \"#r\\\"abc\\\"\") ; => \"abc\"\n(read1 \"#r\\\"[a-z]+\\\"\") ; => \"[a-z]+\"\n\n;; Multiple datums\n(jerboa-read-string \"#r\\\"abc\\\" #r\\\"\\\\d+\\\"\") ; => (\"abc\" \"\\\\d+\")\n\n;; To get annotated datums (with source location), pass a path:\n(define (read1-annotated str)\n (let ((result (car (jerboa-read-string str \"test.ss\"))))\n (annotated-datum-value result))) ; now annotated-datum-value works\n\n;; Raw string semantics:\n;; - Backslashes are literal (no escape processing)\n;; - \\\" is the ONLY escape (produces a literal double-quote)\n;; - LIMITATION: cannot end a raw string with \\\" since it's consumed as escape\n;; e.g. #r\"\\\\\" = backslash + escaped-quote (unterminated), not two backslashes\n;; Workaround: put backslash-pairs mid-string: #r\"a\\\\b\" => \"a\\\\b\" (4 chars)") ("id" . "raw-string-reader-test") + ";; #r\"...\" raw strings are parsed by the Jerboa reader, not Chez's default reader.\n;; In .ss files run with jerboa run, you CANNOT use #r\"...\" literals directly.\n;; To test raw string parsing, call jerboa-read-string on a source string.\n\n;; KEY: without a path argument, jerboa-read-string returns PLAIN values (not annotated datums)\n(define (read1 str)\n (car (jerboa-read-string str))) ; <-- just car, NOT annotated-datum-value\n\n(read1 \"#r\\\"\\\\d+\\\"\") ; => \"\\\\d+\" (the 3-char string backslash,d,plus)\n(read1 \"#r\\\"abc\\\"\") ; => \"abc\"\n(read1 \"#r\\\"[a-z]+\\\"\") ; => \"[a-z]+\"\n\n;; Multiple datums\n(jerboa-read-string \"#r\\\"abc\\\" #r\\\"\\\\d+\\\"\") ; => (\"abc\" \"\\\\d+\")\n\n;; To get annotated datums (with source location), pass a path:\n(define (read1-annotated str)\n (let ((result (car (jerboa-read-string str \"test.ss\"))))\n (annotated-datum-value result))) ; now annotated-datum-value works\n\n;; Raw string semantics:\n;; - Backslashes are literal (no escape processing)\n;; - \\\" is the ONLY escape (produces a literal double-quote)\n;; - LIMITATION: cannot end a raw string with \\\" since it's consumed as escape\n;; e.g. #r\"\\\\\" = backslash + escaped-quote (unterminated), not two backslashes\n;; Workaround: put backslash-pairs mid-string: #r\"a\\\\b\" => \"a\\\\b\" (4 chars)") ("id" . "raw-string-reader-test") ("imports" "(jerboa reader)") ("notes" . - "The #r\"...\" syntax is available when using the Jerboa reader (e.g. in the Jerboa REPL or when reading files with jerboa-read-all). In tests run via scheme --script, use jerboa-read-string to verify raw string parsing. The annotated-datum-value function only works when jerboa-read-string is called WITH a path argument (second arg); without it, values are returned plain.") + "The #r\"...\" syntax is available when using the Jerboa reader (e.g. in the Jerboa REPL or when reading files with jerboa-read-all). In tests run via jerboa run, use jerboa-read-string to verify raw string parsing. The annotated-datum-value function only works when jerboa-read-string is called WITH a path argument (second arg); without it, values are returned plain.") ("tags" "reader" "raw-string" "jerboa-read-string" "annotated-datum" "test") ("title" @@ -2649,7 +2649,7 @@ "(std csp ops)" "(std csp clj)") ("notes" . - "Jerboa's CSP modules are split into four libraries:\n\n- (std csp) — primitives: make-channel, chan-put!, chan-get!,\n make-channel/sliding, make-channel/dropping,\n chan-try-put!/chan-try-get, chan-close!, go,\n chan-pipe, chan-map, chan-filter.\n- (std csp select) — alts!/alts!! (symbol options 'priority / 'default),\n alt!/alt!! macros, timeout channel, chan-recv-evt.\n- (std csp ops) — to-chan, chan-merge, chan-split, chan-pipe-to,\n make-mult/tap!/untap!, make-pub/sub!/unsub!,\n chan-pipeline, chan-pipeline-async,\n make-promise-channel.\n- (std csp clj) — Clojure-named surface over the three modules\n above: chan, >!, <!, >!!, <!!, close!, poll!,\n offer!, alts!/alt!, go, go-loop, merge, split,\n pipe, mult, tap, pub, sub, pipeline,\n promise-chan, sliding-buffer, dropping-buffer.\n\nImporting all four in one file requires shadowing `merge` from\n(chezscheme) (which is Chez's sorted-merge) and `go`/`go-named` from\n(std csp) (which are plain functions, not the clj go macro). Use:\n\n (import (except (chezscheme) merge)\n (except (std csp) go go-named)\n (std csp select)\n (std csp ops)\n (std csp clj))\n\nJerboa's reader rewrites a leading `:` into a module path, so\nClojure's `:priority` / `:default` keyword options don't work — use\nthe plain symbols `'priority` and `'default` instead. The `default`\nclause head in alt!!/alt! is an exported auxiliary keyword, not a\nsymbol, so you must actually import it (it's in both (std csp clj)\nand (std csp select)).\n\nIn Jerboa every `go` is a real OS thread — there is no CPS transform\nand no park/block distinction, so `>!` and `>!!` (and `<!`/`<!!`)\ncollapse to the same operation. That means you can't spawn millions\nof go blocks the way Clojure's core.async does; a few thousand is a\nreasonable ceiling. For low-rate timeouts the `timeout` helper is\nfine; for many short-lived timeouts a timer wheel would be better\n(tracked as Phase 4 in core-async.md).") + "Jerboa's CSP modules are split into four libraries:\n\n- (std csp) — primitives: make-channel, chan-put!, chan-get!,\n make-channel/sliding, make-channel/dropping,\n chan-try-put!/chan-try-get, chan-close!, go,\n chan-pipe, chan-map, chan-filter.\n- (std csp select) — alts!/alts!! (symbol options 'priority / 'default),\n alt!/alt!! macros, timeout channel, chan-recv-evt.\n- (std csp ops) — to-chan, chan-merge, chan-split, chan-pipe-to,\n make-mult/tap!/untap!, make-pub/sub!/unsub!,\n chan-pipeline, chan-pipeline-async,\n make-promise-channel.\n- (std csp clj) — Clojure-named surface over the three modules\n above: chan, >!, <!, >!!, <!!, close!, poll!,\n offer!, alts!/alt!, go, go-loop, merge, split,\n pipe, mult, tap, pub, sub, pipeline,\n promise-chan, sliding-buffer, dropping-buffer.\n\nImporting all four in one file requires shadowing `merge` from\n(chezscheme) (which is Chez's sorted-merge) and `go`/`go-named` from\n(std csp) (which are plain functions, not the clj go macro). Use:\n\n (import (except (chezscheme) merge)\n (except (std csp) go go-named)\n (std csp select)\n (std csp ops)\n (std csp clj))\n\nJerboa's reader rewrites a leading `:` into a module path, so\nClojure's `:priority` / `:default` keyword options don't work — use\nthe plain symbols `'priority` and `'default` instead. The `default`\nclause head in alt!!/alt! is an exported auxiliary keyword, not a\nsymbol, so you must actually import it (it's in both (std csp clj)\nand (std csp select)).\n\nIn Jerboa every `go` is a real OS thread — there is no CPS transform\nand no park/block distinction, so `>!` and `>!!` (and `<!`/`<!!`)\ncollapse to the same operation. That means you can't spawn millions\nof go blocks the way Clojure's core.async does; a few thousand is a\nreasonable ceiling. For low-rate timeouts the `timeout` helper is\nfine; for many short-lived timeouts a timer wheel would be better\n(tracked in the CSP and async docs).") ("tags" "csp" "core.async" "channel" "go" "alts" "clojure" "concurrency" "select" "timeout" "pipeline") ("title" @@ -2665,7 +2665,7 @@ "(std csp clj)") ("notes" . - "Ported verbatim from the Clojure core.async idiom, with three Jerboa-specific tweaks:\n\n1. Clojure's `(nil? v)` becomes `(eof-object? v)` — closed channels yield an eof object, not nil, because Jerboa has no nil/#f distinction and eof is the universal \"stream ended\" sentinel.\n\n2. Clojure's `@out` becomes `(deref out)`. Jerboa has no `@`-prefix reader sugar for atom deref.\n\n3. Clojure's `(swap! out conj v)` becomes `(swap! out (cut cons v <>))`. swap! is variadic and threads the CURRENT atom value as the first arg of the update fn: `(swap! a f x)` runs `(f @a x)`. For `cons` that gives `(cons old v)` — a dotted pair, not a prepended list. The `cut` form from the prelude lets you flip the argument order cleanly: `(cut cons v <>)` is `(lambda (xs) (cons v xs))`.\n\nShadowing notes: `merge` is shadowed from `(chezscheme)` because `(std csp clj)` re-exports a 2-arg channel-merge; `go` and `go-named` are shadowed from `(std csp)` because `(std csp clj)` exports them as macros that spawn threads returning a result channel.\n\nThe `timeout` channel spawns one helper thread per call. That's fine for a few hundred outstanding timeouts per second; for higher rates a timer wheel would be better (tracked as Phase 4 in docs/core-async.md).") + "Ported verbatim from the Clojure core.async idiom, with three Jerboa-specific tweaks:\n\n1. Clojure's `(nil? v)` becomes `(eof-object? v)` — closed channels yield an eof object, not nil, because Jerboa has no nil/#f distinction and eof is the universal \"stream ended\" sentinel.\n\n2. Clojure's `@out` becomes `(deref out)`. Jerboa has no `@`-prefix reader sugar for atom deref.\n\n3. Clojure's `(swap! out conj v)` becomes `(swap! out (cut cons v <>))`. swap! is variadic and threads the CURRENT atom value as the first arg of the update fn: `(swap! a f x)` runs `(f @a x)`. For `cons` that gives `(cons old v)` — a dotted pair, not a prepended list. The `cut` form from the prelude lets you flip the argument order cleanly: `(cut cons v <>)` is `(lambda (xs) (cons v xs))`.\n\nShadowing notes: `merge` is shadowed from `(chezscheme)` because `(std csp clj)` re-exports a 2-arg channel-merge; `go` and `go-named` are shadowed from `(std csp)` because `(std csp clj)` exports them as macros that spawn threads returning a result channel.\n\nThe `timeout` channel spawns one helper thread per call. That's fine for a few hundred outstanding timeouts per second; for higher rates a timer wheel would be better (tracked in the CSP and async docs).") ("related" "csp-core-async-basics" "jerboa-clojure-atom-aliases") @@ -2765,7 +2765,7 @@ ("imports" "(std csp)" "(std csp ops)") ("notes" . - "put! and take! each spawn one OS thread per callback — fine for moderate rates but avoid at high request volumes. For production workloads consider batching callbacks onto a dedicated dispatch thread (see core-async.md §3.4). Callbacks run on the helper thread. If a callback raises, a warning is printed to current-error-port and the helper exits silently — exceptions do NOT propagate to the caller of put!/take!. take! delivers (eof-object) on close, not #f; use (eof-object? v) to detect closure. Also re-exported under the same names from (std csp clj).</notes>\n<parameter name=\"related\">[\"chan-xform-transducer-backed\", \"jerboa-csp-channels\", \"transducer-into-persistent-collections\"]") + "put! and take! each spawn one OS thread per callback — fine for moderate rates but avoid at high request volumes. For production workloads consider batching callbacks onto a dedicated dispatch thread (see docs/async.md and docs/concurrency.md). Callbacks run on the helper thread. If a callback raises, a warning is printed to current-error-port and the helper exits silently — exceptions do NOT propagate to the caller of put!/take!. take! delivers (eof-object) on close, not #f; use (eof-object? v) to detect closure. Also re-exported under the same names from (std csp clj).</notes>\n<parameter name=\"related\">[\"chan-xform-transducer-backed\", \"jerboa-csp-channels\", \"transducer-into-persistent-collections\"]") ("tags" "csp" "channel" "put" "take" "callback" "async" "clojure" "core-async") ("title" @@ -2826,7 +2826,7 @@ "CSP dynamic fan-in (mix / admix / toggle / solo)")) (("code" . - ";; (std csp select) ships two `timeout` implementations:\n;;\n;; - Default: one helper thread per call. Great up to a few\n;; hundred outstanding timeouts per second.\n;; - Wheel: one long-lived timer thread owning a min-heap of\n;; absolute deadlines + a wake-up channel. Enqueue is O(log n),\n;; no per-deadline thread. Appropriate for rate limiting, retry\n;; back-off, I/O deadlines — any workload that fires lots of\n;; short, concurrent timeouts.\n;;\n;; Two ways to use the wheel:\n;;\n;; 1) Flip the default globally by setting the env var at Scheme\n;; start (read once at library load):\n;;\n;; JERBOA_CSP_TIMER_WHEEL=1 scheme --libdirs lib --script app.ss\n;;\n;; Every subsequent `(timeout ms)` routes through the wheel.\n;;\n;; 2) Call `wheel-timeout` directly from `(std csp select)` for\n;; explicit opt-in without restarting. The wheel singleton is\n;; built lazily on first use under a double-checked lock, so\n;; modules that never call it pay nothing.\n\n;; --- Rate limiter: a hundred concurrent deadlines, handled by a\n;; --- single timer thread instead of a hundred short-lived threads.\n(define (deadline-race ms-list)\n ;; Race a list of timeouts — return the one that fires first.\n (let ([chs (map wheel-timeout ms-list)])\n (let ([pick (alts!! chs)])\n ;; pick => (list eof-object winning-ch)\n (cadr pick))))\n\n;; Fire 50 deadlines ranging from 5ms to 50ms, then count how many\n;; had fired by the 30ms mark. Uses the wheel directly.\n(define (stress-demo)\n (let ([chs (let loop ([n 0] [acc '()])\n (if (= n 50)\n (reverse acc)\n (loop (+ n 1)\n (cons (wheel-timeout (+ 5 (* n 1))) acc))))])\n ;; Wait briefly, then check how many deadlines have fired.\n (sleep (make-time 'time-duration (* 30 1000000) 0))\n (length (filter chan-closed? chs))))\n\n;; Using wheel-timeout inside alts!! — same shape as Clojure's\n;; (alts! [ch (timeout N)]): race a normal channel against a\n;; deadline, react to whichever wins.\n(define (recv-with-deadline ch ms)\n (let* ([t (wheel-timeout ms)]\n [pick (alts!! (list ch t))]\n [val (car pick)]\n [won (cadr pick)])\n (cond\n [(eq? won t) 'timed-out]\n [(eof-object? val) 'channel-closed]\n [else val])))\n\n(displayln (stress-demo)) ;; → 50 (all fired within ~30ms)\n(displayln\n (recv-with-deadline (make-channel) 10)) ;; → timed-out") ("id" . "jerboa-csp-timer-wheel") + ";; (std csp select) ships two `timeout` implementations:\n;;\n;; - Default: one helper thread per call. Great up to a few\n;; hundred outstanding timeouts per second.\n;; - Wheel: one long-lived timer thread owning a min-heap of\n;; absolute deadlines + a wake-up channel. Enqueue is O(log n),\n;; no per-deadline thread. Appropriate for rate limiting, retry\n;; back-off, I/O deadlines — any workload that fires lots of\n;; short, concurrent timeouts.\n;;\n;; Two ways to use the wheel:\n;;\n;; 1) Flip the default globally by setting the env var at Scheme\n;; start (read once at library load):\n;;\n;; JERBOA_CSP_TIMER_WHEEL=1 jerboa run app.ss\n;;\n;; Every subsequent `(timeout ms)` routes through the wheel.\n;;\n;; 2) Call `wheel-timeout` directly from `(std csp select)` for\n;; explicit opt-in without restarting. The wheel singleton is\n;; built lazily on first use under a double-checked lock, so\n;; modules that never call it pay nothing.\n\n;; --- Rate limiter: a hundred concurrent deadlines, handled by a\n;; --- single timer thread instead of a hundred short-lived threads.\n(define (deadline-race ms-list)\n ;; Race a list of timeouts — return the one that fires first.\n (let ([chs (map wheel-timeout ms-list)])\n (let ([pick (alts!! chs)])\n ;; pick => (list eof-object winning-ch)\n (cadr pick))))\n\n;; Fire 50 deadlines ranging from 5ms to 50ms, then count how many\n;; had fired by the 30ms mark. Uses the wheel directly.\n(define (stress-demo)\n (let ([chs (let loop ([n 0] [acc '()])\n (if (= n 50)\n (reverse acc)\n (loop (+ n 1)\n (cons (wheel-timeout (+ 5 (* n 1))) acc))))])\n ;; Wait briefly, then check how many deadlines have fired.\n (sleep (make-time 'time-duration (* 30 1000000) 0))\n (length (filter chan-closed? chs))))\n\n;; Using wheel-timeout inside alts!! — same shape as Clojure's\n;; (alts! [ch (timeout N)]): race a normal channel against a\n;; deadline, react to whichever wins.\n(define (recv-with-deadline ch ms)\n (let* ([t (wheel-timeout ms)]\n [pick (alts!! (list ch t))]\n [val (car pick)]\n [won (cadr pick)])\n (cond\n [(eq? won t) 'timed-out]\n [(eof-object? val) 'channel-closed]\n [else val])))\n\n(displayln (stress-demo)) ;; → 50 (all fired within ~30ms)\n(displayln\n (recv-with-deadline (make-channel) 10)) ;; → timed-out") ("id" . "jerboa-csp-timer-wheel") ("imports" "(except (chezscheme) merge)" "(except (std csp) go go-named)" @@ -2940,7 +2940,7 @@ ("imports" "(jerboa prelude)") ("notes" . - "These names trip up Clojure and Racket porters constantly because Jerboa's `conjoin`/`disjoin` come from the Common Lisp / SRFI-1 tradition rather than Clojure's `every-pred`/`some-fn`. Semantics are identical. `complement` and `negate` are both in the prelude and behave the same way. Clojure's `fnil` (wrap a function so nil args get replaced with defaults) is NOT in Jerboa as of 2026-04-11 — roll your own `(def (fnil f default) (lambda (x . r) (apply f (or x default) r)))` if needed. See `docs/clojure-vs-jerboa.md` §26 for the full Clojure→Jerboa combinator mapping.") + "These names trip up Clojure and Racket porters constantly because Jerboa's `conjoin`/`disjoin` come from the Common Lisp / SRFI-1 tradition rather than Clojure's `every-pred`/`some-fn`. Semantics are identical. `complement` and `negate` are both in the prelude and behave the same way. Clojure's `fnil` (wrap a function so nil args get replaced with defaults) is NOT in Jerboa as of 2026-04-11 — roll your own `(def (fnil f default) (lambda (x . r) (apply f (or x default) r)))` if needed. See `docs/jerboa-for-clojure-devs.md` for Clojure-to-Jerboa guidance.") ("related" "jerboa-clojure-compat") ("tags" "predicate" "combinator" "conjoin" "disjoin" "every-pred" "some-fn" "clojure" "complement" "negate") @@ -3432,7 +3432,7 @@ "\n;; === Option 1: file directive (Jerboa reader only — REPL, jerboa-read-file) ===\n;; Put at the top of any .ss file loaded through the Jerboa reader:\n#!cloj\n;; Now Clojure syntax is active for the rest of this file:\n;; {} → hash-map literal {:a 1 :b 2}\n;; #() → anonymous function #(+ % 1) → (lambda (%1) (+ %1 1))\n;; :name → keyword :foo → #:foo\n;; nil → #f\n;; true/#f → #t / #f\n;; @x → (deref x) (always-on, no mode needed)\n;; #{} → eq-hashtable set #{1 2 3} (always-on)\n\n;; === Option 2: programmatic activation ===\n(import (jerboa cloj))\n(activate-cloj-reader!) ; sets (reader-cloj-mode #t) in current scope\n\n;; === Option 3: use (jerboa clojure) — activates automatically ===\n(import (jerboa clojure))\n;; reader-cloj-mode, fn-literal, activate-cloj-reader! all re-exported\n\n;; === fn-literal examples ===\n;; #(+ % 1) → (lambda (%1) (+ %1 1))\n;; #(str %1 \" \" %2) → (lambda (%1 %2) (str %1 \" \" %2))\n;; #(apply + %&) → (lambda %& (apply + %&))\n;; Can also call fn-literal directly (without reader mode):\n(import (jerboa cloj))\n(def double (fn-literal (* %1 2))) ; same as (lambda (%1) (* %1 2))\n(double 5) ; => 10\n\n;; === Mode query ===\n(reader-cloj-mode) ; → #t or #f\n(reader-cloj-mode #t) ; activate\n(reader-cloj-mode #f) ; deactivate (restore Jerboa defaults)\n") ("id" . "cloj-reader-mode") ("imports" "(jerboa cloj)") ("notes" . - "\nIMPORTANT: Clojure reader syntax ({} #() :name nil/true/false) ONLY works via the Jerboa reader pipeline (REPL, jerboa-read-file, jerboa-read-string). It does NOT work in files run with `scheme --script` or jerbuild-compiled .ss files — those use Chez's native (read port).\n\nSyntax gated on (reader-cloj-mode):\n {} — default=method dispatch {method obj args}, cloj=hash-map literal\n #() — default=vector #(1 2 3), cloj=anonymous function #(+ % 1)\n :name — default=module path :std/sort, cloj=keyword :foo\n nil/true/false — default=symbols, cloj=mapped to #f/#t/#f\n\nAlways-on (no mode needed):\n @x — deref (clojure atom deref)\n #{} — eq-hashtable set literal (hash-set items)\n\nfn-literal: % is alias for %1. %& collects rest args. No nesting — #() inside #() is undefined.\n\nParameter scoping: (reader-cloj-mode #t) in a compiled library body does NOT persist to importing code. Use activate-cloj-reader! as an exported function that callers invoke, or rely on #!cloj at file top.\n") + "\nIMPORTANT: Clojure reader syntax ({} #() :name nil/true/false) ONLY works via the Jerboa reader pipeline (REPL, jerboa-read-file, jerboa-read-string). It does NOT work in files run with `jerboa run` or jerbuild-compiled .ss files — those use Chez's native (read port).\n\nSyntax gated on (reader-cloj-mode):\n {} — default=method dispatch {method obj args}, cloj=hash-map literal\n #() — default=vector #(1 2 3), cloj=anonymous function #(+ % 1)\n :name — default=module path :std/sort, cloj=keyword :foo\n nil/true/false — default=symbols, cloj=mapped to #f/#t/#f\n\nAlways-on (no mode needed):\n @x — deref (clojure atom deref)\n #{} — eq-hashtable set literal (hash-set items)\n\nfn-literal: % is alias for %1. %& collects rest args. No nesting — #() inside #() is undefined.\n\nParameter scoping: (reader-cloj-mode #t) in a compiled library body does NOT persist to importing code. Use activate-cloj-reader! as an exported function that callers invoke, or rely on #!cloj at file top.\n") ("related" "hash-set" "keyword-syntax" "reader-extensions") ("tags" "clojure" "reader" "cloj" "syntax" "compatibility" "fn-literal") @@ -3697,7 +3697,7 @@ "Guard side-effecting code with io! inside dosync")) (("code" . - ";; `(s-instrument 'name)` uses set-top-level-value! to rebind a procedure.\n;; In a `scheme --script file.ss` run, `def` creates a lexical binding\n;; in the program body — set-top-level-value! cannot rewrite it, so\n;; s-instrument silently no-ops and callers run the uninstrumented\n;; function. Workaround: wrap the function body with an explicit\n;; s-valid? / s-explain-str guard that works in any execution mode.\n\n(s-def ::isbn (s-and string? (s-pred (lambda (s) (> (string-length s) 10)))))\n(s-def ::qty (s-and integer? (s-pred positive?)))\n;; Note: s-cat takes alternating tag+spec pairs, not a bare list of specs.\n(s-def ::order (s-cat ':isbn '::isbn ':qty '::qty))\n\n(def (place-order isbn qty)\n (unless (s-valid? '::order (list isbn qty))\n (error 'place-order \"args do not conform\"\n (s-explain-str '::order (list isbn qty))))\n (list 'order isbn qty))\n\n(place-order \"978-0553560732\" 2) ;; => (order \"978-...\" 2)\n;; (place-order \"short\" -1) ;; raises with a readable explanation") ("id" . "spec-validation-in-script-context") + ";; `(s-instrument 'name)` uses set-top-level-value! to rebind a procedure.\n;; In a `jerboa run file.ss` run, `def` creates a lexical binding\n;; in the program body — set-top-level-value! cannot rewrite it, so\n;; s-instrument silently no-ops and callers run the uninstrumented\n;; function. Workaround: wrap the function body with an explicit\n;; s-valid? / s-explain-str guard that works in any execution mode.\n\n(s-def ::isbn (s-and string? (s-pred (lambda (s) (> (string-length s) 10)))))\n(s-def ::qty (s-and integer? (s-pred positive?)))\n;; Note: s-cat takes alternating tag+spec pairs, not a bare list of specs.\n(s-def ::order (s-cat ':isbn '::isbn ':qty '::qty))\n\n(def (place-order isbn qty)\n (unless (s-valid? '::order (list isbn qty))\n (error 'place-order \"args do not conform\"\n (s-explain-str '::order (list isbn qty))))\n (list 'order isbn qty))\n\n(place-order \"978-0553560732\" 2) ;; => (order \"978-...\" 2)\n;; (place-order \"short\" -1) ;; raises with a readable explanation") ("id" . "spec-validation-in-script-context") ("imports" "(std spec)") ("notes" . @@ -4916,11 +4916,11 @@ "Smoke test a local Jerboa module with jerbuild exec and libdirs")) (("code" . - ";; Library API: typed-library form -> deterministic textual LLVM IR\n(import (jerboa typed parser) (jerboa typed llvmir))\n\n(def form\n '(typed-library (sample typed demo)\n (export main)\n (def (sum-to (n : Nat)) : Nat\n (for/fold ((acc 0)) ((i (in-range n)))\n (+ acc i)))\n ;; zero-param Nat/Int/Bool main gets a generated @main (exit status)\n (def (main) : Nat\n (bitwise-and (sum-to 10) 63))))\n\n(display (typed-library-form->llvmir-string form))\n\n;; Shell pipeline (Makefile targets wrap the same steps):\n;; scheme --libdirs lib --script support/typed-llvmir.ss build/typed/llvmir file.ss\n;; make typed-llvmir # emit .ll for the LLVM fixtures\n;; make typed-llvmir-check # llvm-as + opt -passes=verify + opt -O2\n;; make typed-llvmir-smoke # llc + cc, run, expect exit 42\n;; make typed-llvmir-parity # same source through Rust backend, compare exits") ("id" . "typed-llvmir-emit-and-verify") + ";; Library API: typed-library form -> deterministic textual LLVM IR\n(import (jerboa typed parser) (jerboa typed llvmir))\n\n(def form\n '(typed-library (sample typed demo)\n (export main)\n (def (sum-to (n : Nat)) : Nat\n (for/fold ((acc 0)) ((i (in-range n)))\n (+ acc i)))\n ;; zero-param Nat/Int/Bool main gets a generated @main (exit status)\n (def (main) : Nat\n (bitwise-and (sum-to 10) 63))))\n\n(display (typed-library-form->llvmir-string form))\n\n;; Shell pipeline (Makefile targets wrap the same steps):\n;; jerboa run support/typed-llvmir.ss build/typed/llvmir file.ss\n;; make typed-llvmir # emit .ll for the LLVM fixtures\n;; make typed-llvmir-check # llvm-as + opt -passes=verify + opt -O2\n;; make typed-llvmir-smoke # llc + cc, run, expect exit 42\n;; make typed-llvmir-parity # same source through Rust backend, compare exits") ("id" . "typed-llvmir-emit-and-verify") ("imports" "(jerboa typed llvmir)" "(jerboa typed parser)") ("notes" . - "Experimental scalar-only backend (docs/llvmir-backend.md): Nat/Int/Bool/Float, Unit returns, let/begin/if, same-module calls, arith/cmp/equal?/bool, bitwise+shifts, exact->inexact, log2 (llvm.log2.f64), for/fold over in-range. Everything else (String/Bytes/records/variants/match/imports) is rejected with a typed-llvmir error. Signedness lives in operations: Nat -> udiv/icmp ult/lshr, Int -> sdiv/icmp slt/ashr. Float literals emit as IEEE-754 hex (0x4011000000000000 = 4.25). Function symbols: @jt_llvm_<module>__<def>. if branches must agree on one numeric type (positive literals are Nat; compute Int 1 as (/ n n)). Rust backend stays the correctness oracle; LLVM tools found on PATH or Homebrew keg, override with LLVM_BIN=.") + "Experimental scalar-only backend : Nat/Int/Bool/Float, Unit returns, let/begin/if, same-module calls, arith/cmp/equal?/bool, bitwise+shifts, exact->inexact, log2 (llvm.log2.f64), for/fold over in-range. Everything else (String/Bytes/records/variants/match/imports) is rejected with a typed-llvmir error. Signedness lives in operations: Nat -> udiv/icmp ult/lshr, Int -> sdiv/icmp slt/ashr. Float literals emit as IEEE-754 hex (0x4011000000000000 = 4.25). Function symbols: @jt_llvm_<module>__<def>. if branches must agree on one numeric type (positive literals are Nat; compute Int 1 as (/ n n)). Rust backend stays the correctness oracle; LLVM tools found on PATH or Homebrew keg, override with LLVM_BIN=.") ("tags" "typed" "llvmir" "llvm" "backend" "native" "for-fold") ("title" @@ -4949,11 +4949,11 @@ ("title" . "Bind current ports around eval in a TCP REPL")) (("code" . - ";; Lower several typed modules (with imports) into ONE textual LLVM module so\n;; cross-module calls resolve. String/Bytes are { ptr, i64 } fat pointers;\n;; records are by-value structs; bytes-build mallocs a fresh buffer.\n(import (jerboa typed parser) (jerboa typed llvmir))\n\n(def lib\n '(typed-library (demo strbytes)\n (export first-byte)\n (def (first-byte (s : String)) : Nat\n (bytevector-u8-ref (string->utf8 s) 0))))\n\n(def app\n '(typed-library (demo app)\n (export starts-h?)\n (import (demo strbytes))\n (def (starts-h? (s : String)) : Bool\n (= (first-byte s) 104)))) ; 104 = 'h'\n\n;; topo-sorts by imports, emits @jt_llvm_demo_strbytes__first_byte once and\n;; the app's call targets that exact symbol\n(display (typed-library-forms->llvmir-string (list app lib)))\n\n;; Shell: scheme --libdirs lib --script support/typed-llvmir.ss \\\n;; --whole-program out.ll a.ss b.ss\n;; then: llvm-as out.ll | opt -passes=verify | opt -O2 | llc | cc harness.c obj") ("id" . "typed-llvmir-whole-program-strings-records") + ";; Lower several typed modules (with imports) into ONE textual LLVM module so\n;; cross-module calls resolve. String/Bytes are { ptr, i64 } fat pointers;\n;; records are by-value structs; bytes-build mallocs a fresh buffer.\n(import (jerboa typed parser) (jerboa typed llvmir))\n\n(def lib\n '(typed-library (demo strbytes)\n (export first-byte)\n (def (first-byte (s : String)) : Nat\n (bytevector-u8-ref (string->utf8 s) 0))))\n\n(def app\n '(typed-library (demo app)\n (export starts-h?)\n (import (demo strbytes))\n (def (starts-h? (s : String)) : Bool\n (= (first-byte s) 104)))) ; 104 = 'h'\n\n;; topo-sorts by imports, emits @jt_llvm_demo_strbytes__first_byte once and\n;; the app's call targets that exact symbol\n(display (typed-library-forms->llvmir-string (list app lib)))\n\n;; Shell: jerboa run support/typed-llvmir.ss \\\n;; --whole-program out.ll a.ss b.ss\n;; then: llvm-as out.ll | opt -passes=verify | opt -O2 | llc | cc harness.c obj") ("id" . "typed-llvmir-whole-program-strings-records") ("imports" "(jerboa typed parser)" "(jerboa typed llvmir)") ("notes" . - "Buffer ABI: String/Bytes = by-value { ptr, i64 } (data,len), matching a C `struct { const char*; uint64_t; }` on SysV/AAPCS, so a C harness can call kernels directly. string->utf8/utf8->string are value identities (same rep). string literals intern one deduped private constant. records lower to structural { f0ty, ... } structs (insertvalue ctor, extractvalue accessor) and can be for/fold accumulators (phi of struct). bytes-build = malloc + store loop, never freed (short-lived kernels; GC is future work). Duplicate function names across modules are rejected (flat LLVM symbols). NOT supported: variants/match/Option/Result, and crypto prims (sha256/aes-gcm/x25519 stay on RustCrypto FFI — never reimplement crypto in LLVM). Real example: jerboa-secmon `make llvmir-bin` builds 6 pure kernels to a native binary asserted against the Rust backend. See docs/llvmir-backend.md.") + "Buffer ABI: String/Bytes = by-value { ptr, i64 } (data,len), matching a C `struct { const char*; uint64_t; }` on SysV/AAPCS, so a C harness can call kernels directly. string->utf8/utf8->string are value identities (same rep). string literals intern one deduped private constant. records lower to structural { f0ty, ... } structs (insertvalue ctor, extractvalue accessor) and can be for/fold accumulators (phi of struct). bytes-build = malloc + store loop, never freed (short-lived kernels; GC is current limitation). Duplicate function names across modules are rejected (flat LLVM symbols). NOT supported: variants/match/Option/Result, and crypto prims (sha256/aes-gcm/x25519 stay on RustCrypto FFI — never reimplement crypto in LLVM). Real example: jerboa-secmon `make llvmir-bin` builds 6 pure kernels to a native binary asserted against the Rust backend. See the typed LLVM API entries in api-signatures.") ("tags" "typed" "llvmir" "llvm" "whole-program" "string" "bytes" "record") ("title" @@ -4965,7 +4965,7 @@ ("imports" "(jerboa typed parser)" "(jerboa typed llvmir)") ("notes" . - "Variants box their payload: the ctor sizes the case's field struct via getelementptr-null + ptrtoint, mallocs it, and stores each field (null box for a fieldless case); never freed (like bytes-build), and recursive variants work because the box is a pointer. match lowers to `switch i32 %tag`, each arm GEP+loads its bound fields from the box (wildcards skip the load) and brrs to a join phi; an exhaustive match's default is `unreachable`, an `else` arm fills the default. Option<T> = by-value { i1, T } (tag 1=Some/0=None, payload undef when None). variant-pred / record-pred lower to constant i1 true (the value already has that type post-check). Case tag = declared case index. NOT yet: Result, resources, closures. Real use: jerboa-secmon `make llvmir-bin`. See docs/llvmir-backend.md.") + "Variants box their payload: the ctor sizes the case's field struct via getelementptr-null + ptrtoint, mallocs it, and stores each field (null box for a fieldless case); never freed (like bytes-build), and recursive variants work because the box is a pointer. match lowers to `switch i32 %tag`, each arm GEP+loads its bound fields from the box (wildcards skip the load) and brrs to a join phi; an exhaustive match's default is `unreachable`, an `else` arm fills the default. Option<T> = by-value { i1, T } (tag 1=Some/0=None, payload undef when None). variant-pred / record-pred lower to constant i1 true (the value already has that type post-check). Case tag = declared case index. Current LLVM gaps: Result, resources, closures. Real use: jerboa-secmon `make llvmir-bin`. See the typed LLVM API entries in api-signatures.") ("tags" "typed" "llvmir" "llvm" "variant" "match" "option") ("title" . @@ -5322,7 +5322,7 @@ "Wrap a Rust/C cdylib opaque handle from Jerboa Scheme")) (("code" . - "# Makefile pattern for consuming a sibling Jerboa project that provides\n# generated Scheme modules plus a native cdylib used by those modules.\n\nJERBOA_TERM_ROOT ?= $(abspath ../jerboa-term)\nJERBOA_TERM_LIBDIR ?= $(JERBOA_TERM_ROOT)/lib\n\n# Add the sibling generated library directory to --libdirs so imports like\n# (import (jerboa-term alacritty-term)) or :jerboa-term/alacritty-term resolve.\nLIBDIRS = --libdirs lib:$(JERBOA)/lib:$(JERBOA_TERM_LIBDIR)\n\n# Export a runtime path used by the sibling Scheme wrapper to load its cdylib.\n# The wrapper can also honor an exact JERBOA_TERM_LIB override.\nexport JERBOA_TERM_DIR ?= $(JERBOA_TERM_ROOT)/target/debug\n\n# Build the sibling native library before running tests that import it.\ntest-alacritty-term: build\n\tcd $(JERBOA_TERM_ROOT) && cargo build --lib >/dev/null\n\t$(SCHEME) $(LIBDIRS) --script tests/test-alacritty-term.ss\n\n# In the sibling Scheme wrapper:\n# (define lib-path\n# (or (getenv \"JERBOA_TERM_LIB\")\n# (string-append (or (getenv \"JERBOA_TERM_DIR\") \"../jerboa-term/target/debug\")\n# \"/libjerboa_term_core.\" shlib-ext)))\n# (load-shared-object lib-path)") ("id" . "jerbuild-sibling-libdir-shlib-env") ("imports") + "# Makefile pattern for consuming a sibling Jerboa project that provides\n# generated Scheme modules plus a native cdylib used by those modules.\n\nJERBOA_TERM_ROOT ?= $(abspath ../jerboa-term)\nJERBOA_TERM_LIBDIR ?= $(JERBOA_TERM_ROOT)/lib\n\n# Add the sibling generated library directory to --libdirs so imports like\n# (import (jerboa-term alacritty-term)) or :jerboa-term/alacritty-term resolve.\nLIBDIRS = --libdirs lib:$(JERBOA)/lib:$(JERBOA_TERM_LIBDIR)\n\n# Export a runtime path used by the sibling Scheme wrapper to load its cdylib.\n# The wrapper can also honor an exact JERBOA_TERM_LIB override.\nexport JERBOA_TERM_DIR ?= $(JERBOA_TERM_ROOT)/target/debug\n\n# Build the sibling native library before running tests that import it.\ntest-alacritty-term: build\n\tcd $(JERBOA_TERM_ROOT) && cargo build --lib >/dev/null\n\tjerboa run tests/test-alacritty-term.ss\n\n# In the sibling Scheme wrapper:\n# (define lib-path\n# (or (getenv \"JERBOA_TERM_LIB\")\n# (string-append (or (getenv \"JERBOA_TERM_DIR\") \"../jerboa-term/target/debug\")\n# \"/libjerboa_term_core.\" shlib-ext)))\n# (load-shared-object lib-path)") ("id" . "jerbuild-sibling-libdir-shlib-env") ("imports") ("notes" . "There are two separate paths to wire: the Jerboa module search path (`--libdirs`) and the native dynamic-library path (`JERBOA_TERM_DIR` or an exact `JERBOA_TERM_LIB`). Building the sibling Rust/C library inside the test target prevents stale symbols when the Scheme FFI wrapper was regenerated but the cdylib was not.") @@ -5567,7 +5567,7 @@ "#!/bin/sh\n#|\nJERBOA_QT_DIR=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\nJH=\"$(jerbuild --jerboa-home 2>/dev/null)\"\nif [ -z \"$JH\" ]; then\n echo \"jerbuild --jerboa-home failed\" >&2\n exit 1\nfi\nQT_SHIM_DIR=\"${JERBOA_QT_SHIM_DIR:-$HOME/mine/jerboa-emacs}\"\nexport JERBOA_QT_LIB=\"$JERBOA_QT_DIR\"\nexport JERBOA_QT_SHIM_DIR=\"$QT_SHIM_DIR\"\nexport DYLD_LIBRARY_PATH=\"$JERBOA_QT_DIR:$QT_SHIM_DIR:${DYLD_LIBRARY_PATH:-}\"\nexport LD_LIBRARY_PATH=\"$JERBOA_QT_DIR:$QT_SHIM_DIR:${LD_LIBRARY_PATH:-}\"\nexec jerbuild exec --libdirs \"$JERBOA_QT_DIR/lib:$JH/lib\" \"$0\" \"$@\"\n|#\n\n(import (jerboa-qt qt))\n\n(define (main)\n (with-qt-app app\n (let ((win (qt-main-window-create))\n (label (qt-label-create \"Hello from jerboa-qt\")))\n (qt-label-set-word-wrap! label #t)\n (qt-main-window-set-central-widget! win label)\n (qt-widget-resize! win 240 120)\n (qt-widget-show! win)\n (qt-app-exec! app))))\n\n(main)\n") ("id" . "jerboa-qt-example-wrapper-jerbuild") ("imports") ("notes" . - "Use this wrapper for runnable jerboa-qt `.ss` examples instead of stale `(chez-qt qt)`, `scheme --libdirs`, `CHEZ_QT_DIR`, or `qt_chez_shim.so` patterns. The repo must already be built/transpiled so `lib/jerboa-qt` exists. For headless smoke, run with `QT_QPA_PLATFORM=offscreen` and kill the app after it stays alive long enough to prove startup. Do not run `sh -n` on this polyglot file: shell parses past the `exec` into the Scheme `|#` marker and reports a false syntax error.") + "Use this wrapper for runnable jerboa-qt `.ss` examples instead of stale `(chez-qt qt)`, `jerboa run`, `CHEZ_QT_DIR`, or `qt_chez_shim.so` patterns. The repo must already be built/transpiled so `lib/jerboa-qt` exists. For headless smoke, run with `QT_QPA_PLATFORM=offscreen` and kill the app after it stays alive long enough to prove startup. Do not run `sh -n` on this polyglot file: shell parses past the `exec` into the Scheme `|#` marker and reports a false syntax error.") ("tags" "jerboa-qt" "jerbuild" "wrapper" "ffi" "dyld" "example") ("title" --- a/data/error-fixes.sexp +++ b/data/error-fixes.sexp @@ -89,13 +89,13 @@ ("type" . "Import Conflict")) (("code_example" . - "# Run with explicit libdirs:\nscheme --libdirs ~/mine/jerboa/lib --script my-script.ss") + "# Run with explicit libdirs:\njerboa run my-script.ss") ("explanation" . "A file or library path could not be resolved. Check JERBOA_HOME and libdirs.") ("fix" . - "Set JERBOA_HOME correctly. Use --libdirs when running: scheme --libdirs $JERBOA_HOME/lib --script file.ss") + "Set JERBOA_HOME to the Jerboa repo root and run: jerboa run file.ss") ("id" . "file-not-found") ("imports") ("message" . "File or module not found") ("pattern" @@ -105,7 +105,7 @@ ("type" . "File Not Found")) (("code_example" . - "(import (jerboa prelude))\n(import (std sort))\n; Run: scheme --libdirs $JERBOA_HOME/lib --script ...") + "(import (jerboa prelude))\n(import (std sort))\n; Run: jerboa run <file>.ss") ("explanation" . "Jerboa can't find the requested library. The JERBOA_HOME/lib directory isn't in the search path.") @@ -324,13 +324,13 @@ ("type" . "FFI Type Mismatch")) (("code_example" . - "# Method 1: command line flag\nscheme --libdirs ~/mine/jerboa/lib --script my-script.ss\n\n# Method 2: environment variable\nexport CHEZSCHEMELIBDIRS=~/mine/jerboa/lib\nscheme --script my-script.ss") + "# Run through the Jerboa CLI:\njerboa run my-script.ss") ("explanation" . - "Chez Scheme can't find Jerboa libraries because --libdirs isn't pointing to the Jerboa lib directory.") + "Jerboa cannot find its libraries because JERBOA_HOME is wrong or the repo-local runtime has not been bootstrapped.") ("fix" . - "Run with: scheme --libdirs $JERBOA_HOME/lib --script your-file.ss. Or set CHEZSCHEMELIBDIRS environment variable.") + "Run with: jerboa run your-file.ss.") ("id" . "missing-libdirs") ("imports") ("message" . "Library search path not configured") ("pattern" @@ -467,17 +467,17 @@ ("type" . "GC Double-Close fd Bug")) (("code_example" . - ";; HANG in --script mode:\n;; (define srv (start-server! 0)) ;; forks thread\n;; (define conn (connect 127.0.0.1 srv)) ;; hangs!\n\n;; FIX 1: use --program mode in Makefile\n;; scheme --program tests/mytest.ss\n\n;; FIX 2: wrap in begin\n(begin\n (define srv (start-server! 0))\n (sleep-ms 100) ;; thread gets to run\n (define conn (connect \"127.0.0.1\" srv)))") + ";; HANG when top-level threaded setup is split across forms:\n;; (define srv (start-server! 0)) ;; forks thread\n;; (define conn (connect 127.0.0.1 srv)) ;; hangs!\n\n;; FIX: wrap setup and use in one top-level expression under jerboa run.\n(begin\n (define srv (start-server! 0))\n (sleep-ms 100) ;; thread gets to run\n (define conn (connect \"127.0.0.1\" srv)))") ("explanation" . - "In --script mode, Chez compiles and evaluates each top-level form independently. A fork-thread in one form starts a thread, but the thread doesn't get CPU time until the main thread blocks or yields. Consecutive top-level defines don't yield, so the thread is never scheduled. The next form that tries to use the thread's result hangs.") + "Jerboa run uses Chez script evaluation internally, where each top-level form is compiled and evaluated independently. A fork-thread in one form may not get CPU time before the next top-level form tries to use its result.") ("fix" . - "Use --program mode for test files with threads. Or wrap everything in a single (begin ...) or (let ...) form. Or use (thread-yield) between forms.") + "Wrap threaded setup and use in a single (begin ...) or (let ...) form, or insert an explicit blocking/yield point before using the thread-owned resource.") ("id" . "jerboa-script-thread-hang") ("imports") ("message" . - "Forked thread doesn't run between top-level form evaluations in --script mode") + "Forked thread doesn't run between separate top-level forms under jerboa run") ("pattern" . "thread.*hang|fork-thread.*connect|server.*not.*accepting|accept.*never") @@ -2122,7 +2122,7 @@ ("type" . "api")) (("code_example" . - ";; Supporting sanity check:\n;; jerboa_verify {\"file_path\":\"script.ss\"}\n;;\n;; Final behavioral check for a CLI script:\n;; scheme --libdirs \"$PWD:$JERBOA_HOME/lib\" --script script.ss -- ARG < input.txt\n;; or the repository's configured `make test` target.") + ";; Supporting sanity check:\n;; jerboa_verify {\"file_path\":\"script.ss\"}\n;;\n;; Final behavioral check for a CLI script:\n;; jerboa run script.ss ARG < input.txt\n;; or the repository's configured `make test` target.") ("explanation" . "`jerboa_verify` can execute enough top-level script code to trigger argument validation without the task's required argv/stdin. That output is useful context, but it is not the final behavioral verifier for executable CLI scripts.") @@ -2286,13 +2286,13 @@ ("type" . "module-resolution")) (("code_example" . - "make chez\nPATH=\"$PWD/.chez/bin:$PATH\" scheme --libdirs lib --script tests/test-core.ss") + "make chez\njerboa run tests/test-core.ss") ("explanation" . - "MCP verifier tools shell out to `scheme`. In a fresh checkout or client environment, the repo may not yet have `.chez/bin/scheme`, and the MCP process may not inherit a PATH containing any Scheme executable.") + "MCP verifier tools use the repo-local Chez runtime internally. In a fresh checkout or client environment, that runtime may not have been bootstrapped yet.") ("fix" . - "Install or bootstrap Chez so `scheme` is on PATH, or run the repo build target that creates `.chez/bin/scheme` before using MCP verifier tools. If verifying manually, use the repo-local binary path after bootstrap, e.g. `.chez/bin/scheme --libdirs lib --script <file>.ss`.") + "Run `make chez` if the repo-local runtime has not been built, then use `jerboa run <file>.ss` for manual verification. MCP verifier tools may still use the repo-local Chez binary internally.") ("id" . "scheme-command-not-found-verifier") ("pattern" . "/bin/sh: scheme: command not found") ("type" . "tooling")) --- a/data/features.sexp +++ b/data/features.sexp @@ -37,7 +37,7 @@ "~1500 tokens per doc-ship session (replaces: write tmp file → read → run → parse error → re-write → re-run → parse → fix → re-run cycle). Multiplicative across every code block in a large doc.") ("example_scenario" . - "docs/core-async.md Section 17 contains a 20-line worked example that ports Clojure's scatter/gather pattern to Jerboa. Two lines use `@out` (Clojure atom-deref reader sugar that doesn't exist in Jerboa) and one uses `(swap! out cons v)` instead of `(swap! out (cut cons v <>))`. A `jerboa_doc_verify docs/core-async.md` call would have compiled the block, noted \"variable @out is not bound\" at the fence's line, and caught both errors in one shot. Instead I wrote a /tmp/test-async-example.ss file, ran it by hand, saw the first error, fixed it, re-ran, saw the second, fixed it, re-ran again. Three iterations that one tool call could collapse into one. Same friction applies to every cookbook recipe on disk and every README code snippet.") + "docs/async.md Section 17 contains a 20-line worked example that ports Clojure's scatter/gather pattern to Jerboa. Two lines use `@out` (Clojure atom-deref reader sugar that doesn't exist in Jerboa) and one uses `(swap! out cons v)` instead of `(swap! out (cut cons v <>))`. A `jerboa_doc_verify docs/async.md` call would have compiled the block, noted \"variable @out is not bound\" at the fence's line, and caught both errors in one shot. Instead I wrote a /tmp/test-async-example.ss file, ran it by hand, saw the first error, fixed it, re-ran, saw the second, fixed it, re-ran again. Three iterations that one tool call could collapse into one. Same friction applies to every cookbook recipe on disk and every README code snippet.") ("id" . "doc-code-block-verify") ("impact" . "high") ("implemented_in" . "mcp/server.ss") ("implemented_tool" . "jerboa_doc_verify") @@ -49,7 +49,7 @@ "Verify Jerboa code blocks inside markdown docs compile and run") ("use_case" . - "When shipping a design doc or porting guide that contains many worked code examples, before committing. Also for periodic audits of existing docs to catch drift when APIs change. Would have prevented shipping docs/core-async.md with two broken references (`@out` reader sugar that doesn't exist, `(swap! out cons v)` with wrong argument order) — both only discovered by running the example manually after commit.") + "When shipping a design doc or porting guide that contains many worked code examples, before committing. Also for periodic audits of existing docs to catch drift when APIs change. Would have prevented shipping docs/async.md with two broken references (`@out` reader sugar that doesn't exist, `(swap! out cons v)` with wrong argument order) — both only discovered by running the example manually after commit.") ("votes" . 2)) (("closed_reason" . @@ -181,7 +181,7 @@ "~600 tokens per doc-verification pass (replaces 4-6 grep calls + result inspection with 1 structured tool call). Higher for porting workflows that cross-check dozens of names.") ("example_scenario" . - "While updating `docs/clojure-vs-jerboa.md` to add status markers, I needed to verify whether Clojure names like `merge-with`, `zipmap`, `reduce-kv`, `memoize`, `iterate`, `repeatedly`, `fnil`, `every-pred`, `some-fn`, `trampoline`, `min-key`, `max-key`, `min-by`, `max-by`, `iterate-n` exist in Jerboa. I ran 5 separate `Grep` calls across `lib/` to check — a total of ~15 symbol lookups. A single `jerboa_symbol_exists_batch` call with the list would have returned `{select-keys: exists, merge: exists, everything-else: absent}` in one shot. Also discovered that the existing doc had several stale claims about non-existent functions (iterate-n, min-by, max-by, fnil-in-prelude, every-pred-in-prelude) because no one had cheap way to sanity-check a batch of names.") + "While updating `docs/jerboa-for-clojure-devs.md` to add status markers, I needed to verify whether Clojure names like `merge-with`, `zipmap`, `reduce-kv`, `memoize`, `iterate`, `repeatedly`, `fnil`, `every-pred`, `some-fn`, `trampoline`, `min-key`, `max-key`, `min-by`, `max-by`, `iterate-n` exist in Jerboa. I ran 5 separate `Grep` calls across `lib/` to check — a total of ~15 symbol lookups. A single `jerboa_symbol_exists_batch` call with the list would have returned `{select-keys: exists, merge: exists, everything-else: absent}` in one shot. Also discovered that the existing doc had several stale claims about non-existent functions (iterate-n, min-by, max-by, fnil-in-prelude, every-pred-in-prelude) because no one had cheap way to sanity-check a batch of names.") ("id" . "batch-symbol-existence-check") ("impact" . "medium") ("implemented_in" . "mcp/server.ss") ("implemented_tool" . "jerboa_symbol_exists_batch") @@ -284,7 +284,7 @@ "~300 tokens per verify session on script files (eliminates fallback to make check + bash startup test)") ("example_scenario" . - "jerboa-edge/edge.ss starts with #!/usr/bin/env -S scheme --libdirs lib --script. Running jerboa_verify on it returns \"Exception: invalid syntax scheme\" — a false positive. The only way to confirm the file is valid is to run make check (which only checks that the scheme binary runs, not the file itself) or start the server via bash. This costs 2-3 extra tool calls per edit cycle.") + "Legacy edge scripts used a raw Scheme shebang; current scripts should use #!/usr/bin/env -S jerboa run. Running jerboa_verify on it returns \"Exception: invalid syntax scheme\" — a false positive. The only way to confirm the file is valid is to run make check (which only checks that the scheme binary runs, not the file itself) or start the server via bash. This costs 2-3 extra tool calls per edit cycle.") ("id" . "verify-shebang-script-support") ("impact" . "medium") ("implemented_in" . "mcp/server.ss") ("implemented_tool" . "jerboa_verify") @@ -360,7 +360,7 @@ "~2000 tokens per doc review session (eliminates multiple grep/find/agent cycles to verify claims; prevents entire sessions wasted on false assumptions about what's already built)") ("example_scenario" . - "docs/jerboa-db.md claimed \"All 8 phases implemented (31 files, ~6200 lines, 34/34 tests passing)\" with every scorecard row marked \"Done\". In reality, zero of the 31 src/jerboa-db/*.ss files existed. Discovering this required: (1) grep -r for datom/datomic across lib/src/tests (found nothing), (2) spawning an Explore agent to search comprehensively, (3) manually checking src/jerboa-db/ directory existence. A single `jerboa_doc_status_audit docs/jerboa-db.md` would have returned \"0/31 claimed files exist, 52 features marked Done with no source\" in one call.") + "The deleted Jerboa DB design doc claimed \"All 8 phases implemented (31 files, ~6200 lines, 34/34 tests passing)\" with every scorecard row marked \"Done\". In reality, zero of the 31 src/jerboa-db/*.ss files existed. Discovering this required: (1) grep -r for datom/datomic across lib/src/tests (found nothing), (2) spawning an Explore agent to search comprehensively, (3) manually checking src/jerboa-db/ directory existence. A single `jerboa_doc_status_audit <doc>` would have returned \"0/31 claimed files exist, 52 features marked Done with no source\" in one call.") ("id" . "doc-implementation-status-audit") ("impact" . "high") ("implemented_in" . "mcp/server.ss") ("implemented_tool" . "jerboa_doc_status_audit") @@ -1259,13 +1259,13 @@ "Out-of-range and char-offset verifier diagnostics are now clamped back to the available source range and rendered with a nearby source excerpt.") ("description" . - "jerboa_verify crashed with an out-of-range string-ref while verifying a large user-facing .ss file. The caller then had to fall back to direct scheme --script checks, losing the normal combined verify report.") + "jerboa_verify crashed with an out-of-range string-ref while verifying a large user-facing .ss file. The caller then had to fall back to direct jerboa run checks, losing the normal combined verify report.") ("estimated_token_reduction" . "~300-800 tokens per failure by avoiding fallback command checks and manual explanation.") ("example_scenario" . - "Verifying search.ss (~34 KB) raised: Exception in string-ref: 34389 is not a valid index. The same file loaded successfully via scheme --script and jsh ,use.") + "Verifying search.ss (~34 KB) raised: Exception in string-ref: 34389 is not a valid index. The same file loaded successfully via jerboa run and jsh ,use.") ("id" . "verify-large-file-range-guard") ("impact" . "medium") ("implemented_in" . "mcp/server.ss") ("implemented_tool" . "jerboa_verify") --- a/docs/chez-hardening.md +++ b/docs/chez-hardening.md @@ -499,10 +499,9 @@ with the Jerboa shim, closing the gap `secure.md` describes as - `secure.md` — overall security strategy; this doc is the Path-1 detail it currently lacks. -- `docs/Future-proofing.md` §3.8 — strategic argument for shrinking - the Chez TCB; this doc is the *defensive* answer (harden what's - there) complementing the *offensive* answer (replace it with s7 or - shift to WASM) the doc considers. +- `docs/native-rust.md` and `docs/status.md` — current native-boundary + policy and release status. This doc is the defensive answer: harden + the Chez runtime that remains in the trust boundary. - `docs/chez-fork.md` — narrative on what the fork has already added; Phase 1 here exists because of patches `829bc806` and `beefa2a6`. --- a/mcp/plan.md +++ b/mcp/plan.md @@ -187,7 +187,7 @@ Port these first because most tools depend on them: 1. `response.ss`: helpers for text responses, error responses, and JSON-RPC responses. 2. `filesystem.ss`: read/write files, recursive scans, extension filters, skip dirs, path normalization. -3. `process.ss`: resolve Jerboa home, resolve `scheme`, run subprocesses with timeout, run Makefile targets, capture stdout/stderr/exit code. +3. `process.ss`: resolve Jerboa home, resolve the repo-local runtime, run subprocesses with timeout, run Makefile targets, capture stdout/stderr/exit code. 4. `runtime-script.ss`: build eval, syntax-check, compile-check, and macro-introspection scripts as strings. 5. `registry.ss`: register tools, aliases, modes, critical list, dispatcher. 6. `schema.ss`: validate MCP arguments and format schema descriptions. @@ -354,7 +354,8 @@ Final acceptance: - `make test` passes using only Jerboa. - `make build` or equivalent creates no JavaScript, TypeScript, `node_modules`, `dist`, or `.sls` artifacts. -- A real MCP client can add this server as `scheme --libdirs <jerboa>/lib --script bin/jerboa-mcp.ss`. +- A real MCP client can add this server through the Jerboa CLI wrapper; the + server may still use Chez internally for verifier subprocesses. - The source repo can be deleted from `PATH`/npm context and the pure Jerboa server still runs. ## Implementation Order --- a/mcp/server.ss +++ b/mcp/server.ss @@ -3688,7 +3688,7 @@ (def rules (list (cons "(export #t)" "Jerboa exports must be explicit, not #t.") (cons ":gerbil/" "Gerbil module path found; use Jerboa/std module path.") - (cons "gxi" "Gerbil gxi command found; use scheme --libdirs ... --script.") + (cons "gxi" "Gerbil gxi command found; use jerboa run for user-facing scripts.") (cons "gxc" "Gerbil gxc command found; use Jerboa/Chez build flow.") (cons "##" "Gambit/Gerbil primitive marker found; requires manual port."))) (filter-map @@ -5157,28 +5157,27 @@ (rank-recipes matches query))) (def (shell-verify-command kind task file project) - (let* ([scheme (recommended-scheme-path #f)] - [target (advisor-file (jhash "file_path" file) "main.ss")]) + (let* ([target (advisor-file (jhash "file_path" file) "main.ss")]) (cond [(string=? kind "docs") "make check-docs"] [(string=? kind "test") - (if project "make test" "scheme --script test.ss")] + (if project "make test" "jerboa run test.ss")] [(string=? kind "module") (if file (string-append "jerbuild compile " file) "make build")] [(string=? kind "script") (if (script-behavior-task? task) - (string-append scheme " --script " target) - (string-append scheme " --script " target))] + (string-append "jerboa run " target) + (string-append "jerboa run " target))] [(string=? kind "debug-error") (if file - (string-append scheme " --script " file) + (string-append "jerboa run " file) "make test")] [else (if file - (string-append scheme " --script " file) + (string-append "jerboa run " file) "make build")]))) (def (mcp-verify-tool-line kind file project) @@ -5591,16 +5590,14 @@ (def (workflow-tool-lines kind project file) (cond [(string=? kind "script") - (let ([scheme (recommended-scheme-path #f)] - [libs (libdirs (jerboa-home #f))] - [script (if file file "path/to/script.ss")]) + (let ([script (if file file "path/to/script.ss")]) (list "1. jerboa_howto {\"query\":\"command-line arguments script\", \"compact\":true}" (string-append "2. jerboa_script_scaffold_verify {\"task\":\"sum two numbers\", \"file_path\":\"" script "\", \"write\":true, \"overwrite\":true, \"args\":[\"2\",\"3\"], \"expected_output\":\"5\"}") "3. If script_scaffold_verify says `Write: written`, do not call write/edit; the file is already present." (string-append "4. jerboa_verify {\"file_path\":\"" script "\"}") - (string-append "5. Run exactly: " scheme " --libdirs " libs " --script " script " 2 3") - "6. Do not use Gerbil or search /opt for an interpreter; use the absolute Scheme path above."))] + (string-append "5. Run exactly: jerboa run " script " 2 3") + "6. Do not use Gerbil or search /opt for an interpreter; use the Jerboa CLI above."))] [(string=? kind "debug-error") (list "1. jerboa_explain_error {\"error_message\":\"<paste exact error>\"}" @@ -5854,10 +5851,8 @@ (def (script-verify-command-string file argv home) (string-append - (recommended-scheme-path home) - " --libdirs " - (path-join (jerboa-home home) "lib") - " --script " + (path-join (jerboa-home home) "bin" "jerboa") + " run " file (if (null? argv) "" (string-append " " (string-join argv " "))))) @@ -5945,7 +5940,7 @@ "Code: omitted because the file was written; pass include_code=true to inspect it.\n\n") "Notes:\n" "- The script runs at top level; it does not rely on a `main` binding.\n" - "- The args normalizer supports both `scheme --script file.ss ...` and wrappers that include file.ss as argv[0].") + "- The args normalizer supports both `jerboa run file.ss ...` and wrappers that include file.ss as argv[0].") failed?))) (def (fence-line? line)