updates
ober
3ac91567a4ec0f5bcef9fc7577a6931dbd9948ef
--- a/data/cookbooks.sexp +++ b/data/cookbooks.sexp @@ -4383,10 +4383,10 @@ "Render an exception condition into a string instead of swallowing it")) (("code" . - ";; The MCP server speaks newline-delimited JSON-RPC over stdio. You can drive it from\n;; bash without going through Claude Code MCP shim - useful for scripting, debugging,\n;; or when the shim is hanging on a particular call shape.\n\n;; Read tool exposed in hybrid mode:\n;; echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa_howto,arguments:{query:hash-table}}} | jmcp\n\n;; Writer tools are critical now, so they are exposed directly in hybrid mode:\n;; echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa_howto_add,arguments:{id:foo,title:t,code:c}}} | jmcp\n\n;; Compact dispatcher route, useful in mini mode or for lazy tool discovery:\n;; echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa,arguments:{tool:search,args:{query:benchmark}}}} | jmcp\n\n;; Pattern for batch saves with status check (bash):\n;; call() { resp=$(echo $1 | timeout 15 /path/to/jmcp | head -1); echo $resp | python3 -c\n;; import sys, json; r = json.loads(sys.stdin.read());\n;; print(OK if isError not in str(r) else ERR, r[result][content][0][text][:120]); }") ("id" . "jmcp-direct-jsonrpc-pipe") ("imports") + ";; The MCP server speaks newline-delimited JSON-RPC over stdio. You can drive it from\n;; bash without going through Claude Code MCP shim - useful for scripting, debugging,\n;; or when the shim is hanging on a particular call shape.\n\n;; Set JERBOA_MCP_REPO when running jmcp outside the Jerboa repo; otherwise\n;; repo-root defaults to the current directory and tools may read embedded data.\n;; export JERBOA_MCP_REPO=$HOME/mine/jerboa\n\n;; Read tool exposed in hybrid mode:\n;; echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa_howto,arguments:{query:hash-table}}} | jmcp\n\n;; Writer tools are critical now, so they are exposed directly in hybrid mode:\n;; echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa_howto_add,arguments:{id:foo,title:t,code:c}}} | jmcp\n\n;; Compact dispatcher route, useful in mini mode or for lazy tool discovery:\n;; echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa,arguments:{tool:search,args:{query:benchmark}}}} | jmcp\n\n;; Pattern for batch saves with status check (bash):\n;; call() { resp=$(echo $1 | timeout 15 /path/to/jmcp | head -1); echo $resp | python3 -c\n;; import sys, json; r = json.loads(sys.stdin.read());\n;; print(OK if isError not in str(r) else ERR, r[result][content][0][text][:120]); }") ("id" . "jmcp-direct-jsonrpc-pipe") ("imports") ("notes" . - "Default mode is hybrid (env JERBOA_MCP_MODE). Mode full exposes every tool top-level; mode mini exposes only the compact jerboa dispatcher. Writer tools such as jerboa_howto_add and jerboa_suggest_feature are now critical/direct in hybrid mode. Dispatcher uses short names (search, catalog, describe, howto_add). Each invocation respawns the server unless you keep a process attached - fine for one-shots. Server reads one request, writes one response, then EOF terminates.") + "Default mode is hybrid (env JERBOA_MCP_MODE). Mode full exposes every tool top-level; mode mini exposes only the compact jerboa dispatcher. Writer tools such as jerboa_howto_add and jerboa_suggest_feature are now critical/direct in hybrid mode. Dispatcher uses short names (search, catalog, describe, howto_add). Each invocation respawns the server unless you keep a process attached - fine for one-shots. Server reads one request, writes one response, then EOF terminates. Set JERBOA_MCP_REPO when invoking jmcp from another repository so data-path points at the live data/ directory instead of falling back to embedded data.") ("tags" "jmcp" "mcp" "json-rpc" "dispatcher" "pipe" "scripting") ("title" @@ -4614,6 +4614,150 @@ "Useful for vendored R6RS/Chez libraries when older consumers still import or exclude a legacy export name. Exporting `(rename (canonical legacy))` lets `(except (module) legacy)` remain valid without defining a second binding in the library body.") ("tags" "library" "export" "rename" "compatibility" "alias" "chezscheme") + ("title" . "Export compatibility aliases with R6RS rename")) + (("code" + . + ";; Single binding — list, vector, or any Iterable\n(for (x '(1 2 3))\n (displayln x))\n\n;; in-range: (end), (start end), or (start end step)\n(for (x (in-range 5)) ; 0 1 2 3 4\n (displayln x))\n(for (x (in-range 4 7)) ; 4 5 6\n (displayln x))\n(for (x (in-range 5 10 2)) ; 5 7 9\n (displayln x))\n(for (x (in-range 7 4 -1)) ; 7 6 5 (auto-decrements)\n (displayln x))\n\n;; Parallel iteration (stops at shortest)\n(for ((x '(1 2 3)) (y '#(a b c d)))\n (displayln x \" \" y)) ; 1 a / 2 b / 3 c\n\n;; in-range-inclusive (includes end)\n(for (x (in-range-inclusive 3)) ; 0 1 2 3\n (displayln x))\n\n;; Iterate over vectors explicitly\n(for (x (in-vector '#(10 20 30)))\n (displayln x))\n\n;; Coroutine iterator (generator pattern)\n(def (my-gen n)\n (lambda (yield)\n (let lp ((k 0))\n (when (< k n) (yield k) (lp (1+ k))))))\n(for (x (my-gen 3))\n (displayln x)) ; 0 1 2") ("id" . "v019-iter-for-basics") ("imports" ":std/iter") + ("notes" + . + "v0.19 rewrites std/iter from scratch around a new Iterator interface protocol. Key changes from v0.18: (1) in-iota is removed — use in-range; (2) the iter-end sentinel is gone — iterators now signal end with #!eof; (3) the iterator struct is replaced by the Iterator interface (next! method); (4) in-range auto-detects direction when no step given: counts up if end>start, down if end<start; (5) lists, vectors, and procedures are all Iterable.") + ("tags" "iter" "for" "iteration" "in-range" "in-list" + "v0.19") + ("title" . "v0.19: Basic for loop with iterators")) + (("code" + . + ";; for/collect — build a list (skips #!void results)\n(for/collect (x (in-range 5))\n (* x x)) ; => (0 1 4 9 16)\n\n;; Multi-binding for/collect (double parens for multiple)\n(for/collect ((x '(1 2 3)) (y '#(a b c d)))\n (cons x y)) ; => ((1 . a) (2 . b) (3 . c))\n\n;; for/fold — accumulate a value\n(for/fold (r []) ((x '(1 2 3)))\n (cons x r)) ; => (3 2 1)\n\n(for/fold (sum 0) (x (in-range 5))\n (+ sum x)) ; => 10\n\n;; with/when filter\n(for/collect (x (in-range 5) when (odd? x))\n x) ; => (1 3)\n\n;; when/unless in multi-binding (after bindings)\n(for/collect ((x (in-range 5)) (y '(a b c d e)) when (odd? x))\n (cons x y)) ; => ((1 . b) (3 . d))\n\n(for/fold (r []) ((x (in-range 5)) unless (odd? x))\n (cons x r)) ; => (4 2 0)\n\n;; Per-binding filter (inside the binding form)\n(for/collect ((x '(1 2 3 4) when (odd? x)))\n x) ; => (1 3)") ("id" . "v019-iter-collect-fold") ("imports" ":std/iter") + ("notes" + . + "Two filter placement styles in v0.19: (1) after the binding form as `(for/collect (x expr when pred) body)` — applies to single-binding; (2) after all bindings as `(for/collect ((x e1) (y e2) when pred) body)` — filters the combined iteration. for/fold accumulator init `iv` can be any value; body should return the next accumulator value (returning #!void skips the update). for/collect skips #!void returns.") + ("tags" "iter" "for/collect" "for/fold" "comprehension" + "collect" "v0.19") + ("title" + . + "v0.19: for/collect and for/fold comprehensions")) + (("code" + . + ";; The Iterator interface has one method: (next!) => :t\n;; Returns #!eof when exhausted (replaces v0.18 iter-end sentinel)\n(interface Iterator\n (next!) => :t)\n\n(interface Iterable\n (iter) => Iterator)\n\n;; Implement a custom countdown iterator\n(defstruct countdown\n ((n :- :fixnum))\n final: #t)\n\n(implement Iterator countdown\n (next!\n (lambda (self)\n (if (fx> self.n 0)\n (let (v self.n)\n (set! self.n (fx- self.n 1))\n v)\n #!eof))))\n\n;; Wrap in Iterator interface for use with for\n(def (in-countdown n)\n => Iterator\n (Iterator (countdown n)))\n\n;; Use it\n(for (x (in-countdown 3))\n (displayln x)) ; 3 2 1\n\n(for/collect (x (in-countdown 5)) x) ; => (5 4 3 2 1)\n\n;; Make a type Iterable by implementing Iterable interface\n(defstruct my-range ((lo :- :fixnum) (hi :- :fixnum)))\n\n(implement Iterable my-range\n (iter (lambda (self)\n (in-range self.lo self.hi))))\n\n;; Now for/for/collect work directly on my-range\n(for (x (iter (my-range 2 5)))\n (displayln x)) ; 2 3 4") ("id" . "v019-iter-custom") ("imports" ":std/iter") + ("notes" + . + "The Iterator interface replaces v0.18's iterator struct. Key differences: (1) return #!eof (not iter-end) to signal end; (2) implement via (implement Iterator <struct> (next! ...)); (3) wrap in Iterator interface with (Iterator instance); (4) procedures (lambdas) are automatically Iterable as coroutines — (lambda (yield) ...) can be passed directly to for. The iter function coerces any Iterable to an Iterator.") + ("tags" "iter" "Iterator" "Iterable" "interface" "custom" + "v0.19") + ("title" . "v0.19: Implement custom Iterator/Iterable")) + (("code" + . + ";; in-hash yields (key . value) cons pairs\n(let (ht (hash (a 1) (b 2) (c 3)))\n (for (pair (in-hash ht))\n (displayln (car pair) \" => \" (cdr pair))))\n\n;; Destructure the pair in a match pattern\n(let (ht (hash (x 10) (y 20)))\n (for ([k . v] (in-hash ht)) ; pattern binding\n (displayln k \" -> \" v)))\n\n;; Keys only\n(let (ht (hash (a 1) (b 2)))\n (for/collect (k (in-hash-keys ht)) k))\n\n;; Values only\n(let (ht (hash (a 1) (b 2)))\n (for/collect (v (in-hash-values ht)) v))\n\n;; HashTable is Iterable — default iteration yields (key . value)\n(let (ht (hash (p 7) (q 8)))\n (for (kv ht)\n (displayln kv)))") ("id" . "v019-iter-hash") ("imports" ":std/iter") + ("notes" + . + "in-hash returns (key . value) cons cells. Hash tables implement Iterable directly (iterating yields pairs). Use pattern binding `([k . v] expr)` to destructure inline. Iteration order is unspecified for hash tables. Works with any HashTable interface instance.") + ("tags" "iter" "hash" "in-hash" "in-hash-keys" + "in-hash-values" "v0.19") + ("title" . "v0.19: Iterate over hash tables")) + (("code" + . + ";; format returns a string; compile-time optimized when fmt is a literal\n(format \"%a\" 42) ; => \"42\" (%a = display)\n(format \"%s\" '(1 2)) ; => \"(1 2)\" (%s = write)\n(format \"Hello %a!\" \"world\") ; => \"Hello world!\"\n\n;; Integer specifiers\n(format \"%d\" 255) ; => \"255\" decimal\n(format \"%x\" #x1234ab) ; => \"#x1234ab\" hex with prefix\n(format \"%X\" #x1234ab) ; => \"1234AB\" hex uppercase, no prefix\n(format \"%b\" #b1001) ; => \"#b1001\" binary with prefix\n(format \"%B\" #b1001) ; => \"1001\" binary, no prefix\n(format \"%o\" #o755) ; => \"#o755\" octal with prefix\n(format \"%O\" #o755) ; => \"755\" octal, no prefix\n\n;; Flonum specifiers\n(format \"%f\" 3.14159) ; => \"3.14159\"\n(format \"%.2f\" 3.14159) ; => \"3.14\"\n(format \"%8.3f\" 3.14) ; => \" 3.140\" (width 8)\n(format \"%-8.3f\" 3.14) ; => \"3.140 \" (left-align)\n(format \"%e\" 12345.6) ; => \"1.23456e4\"\n(format \"%g\" 0.001) ; => \"1e-3\"\n\n;; Multiple args\n(format \"[%d, %d]\" 1 2) ; => \"[1, 2]\"\n(format \"%a/%a\" 'foo 'bar) ; => \"foo/bar\"\n\n;; fprintf — write to a port or BufferedWriter\n(fprintf (current-output-port) \"count: %d\\n\" 42)\n\n;; printf — shorthand for fprintf to current-output-port\n(printf \"x=%d y=%d\\n\" 10 20)\n\n;; eprintf — write to current-error-port\n(eprintf \"error: %a\\n\" 'oops)\n\n;; Runtime format (non-literal string)\n(let (fmt \"%a %a\")\n (format fmt 'hello 'world)) ; => \"hello world\"") ("id" . "v019-format-basics") ("imports" ":std/format") + ("notes" + . + "v0.19 replaces the old :std/format (which used Common Lisp ~a/~s directives) with a new compile-time-optimized format module. Key differences: (1) uses C-style % directives instead of CL-style ~; (2) when the format string is a literal, format/fprintf expand at compile-time to direct writer calls (zero allocation for the format string); (3) lowercase specifiers (%x, %b, %o) include radix prefix (#x, #b, #o), uppercase (%X, %B, %O) omit it; (4) %a = display, %s = write, %q = debug; (5) \\ escape sequences work in the format string (\\n, \\t, etc.). Width/precision/flags (#,-,+,space,0) are supported as in C printf.") + ("tags" "format" "printf" "fprintf" "string" "output" + "v0.19") + ("title" + . + "v0.19: format/fprintf/printf string formatting")) + (("code" + . + ";; make-formater pre-parses a format string into a reusable Formater object\n(def coord-fmt (make-formater \"(%d, %d)\"))\n\n;; apply-formater returns a string\n(apply-formater coord-fmt 3 7) ; => \"(3, 7)\"\n(apply-formater coord-fmt 10 20) ; => \"(10, 20)\"\n\n;; apply-formater-to-output writes to a port/BufferedWriter\n(apply-formater-to-output coord-fmt (current-output-port) 1 2) ; prints \"(1, 2)\"\n\n;; Useful in hot paths: parse fmt once, apply many times\n(def log-line-fmt (make-formater \"%a [%a] %a\"))\n\n(def (emit-log level source msg)\n (apply-formater log-line-fmt level source msg))") ("id" . "v019-format-formater") ("imports" ":std/format") + ("notes" + . + "Use Formater when the same format string is applied many times (e.g., in a logging hot path). make-formater parses the format string once into a Formater struct; apply-formater applies it without re-parsing. Compare to (format \"%d\" x) which, when given a literal string, is compile-time-expanded anyway — Formater is only worth the effort for runtime-constructed or very frequently-applied format strings.") + ("tags" "format" "Formater" "make-formater" "apply-formater" + "v0.19") + ("title" + . + "v0.19: Reusable Formater objects for repeated formatting")) + (("code" + . + ";; Start the system logger (required before any log output)\n;; defaults: sinks=[console-log-sink], level=INFO\n(start-system-logger!)\n\n;; Optional: set level and sinks\n(start-system-logger!\n level: DEBUG\n sinks: [console-log-sink])\n\n;; Default process-level loggers (from :std/log)\n;; Levels: CRITICAL=-1 ERROR=0 WARN=1 INFO=2 DEBUG=3 VERBOSE=4\n(log.info \"server started\" 'port 8080)\n(log.warn \"retrying\" 'attempt 3)\n(log.errorf \"failed after %d attempts\" 5)\n(log.debug \"connection from\" 'addr \"127.0.0.1\")\n\n;; deflogger — create a named sub-logger for a subsystem\n;; The deflogger form creates .info, .warn, .error, .debug etc. macros\n;; prefixed with the logger identifier\n(deflogger myapp name: 'myapp)\n\n;; Now use myapp.info, myapp.warn, etc.\n(myapp.info \"starting\" 'workers 4)\n(myapp.warn \"high load\" 'queue-depth 1000)\n(myapp.errorf \"DB timeout after %dms\" 5000)\n(myapp.debug \"request\" 'method 'GET 'path \"/api/v1/users\")\n\n;; Available level macros for each logger:\n;; .critical .criticalf .error .errorf .warn .warnf\n;; .info .infof .debug .debugf .verbose .verbosef\n;; (f-suffix = format string; plain = display args)\n\n;; Structured key-value args follow the message\n(myapp.info \"user login\" 'user-id 42 'ip \"10.0.0.1\")\n\n;; Control per-subsystem log level\n(user-log-level 'myapp) ; get current level\n(set-user-log-level! 'myapp DEBUG) ; set level") ("id" . "v019-log-deflogger") ("imports" ":std/log") + ("notes" + . + "v0.19 replaces the old :std/logger actor-based logging with a new :std/log package. Key points: (1) must call start-system-logger! before logging anything or output is dropped; (2) deflogger creates a per-subsystem logger bound to a symbol name — the generated macros check level before formatting, so disabled-level log calls have near-zero cost; (3) the f-suffix variants (.infof, .debugf) take a format string (\"%a %d\" style) followed by args; (4) plain variants (.info, .debug) take a message followed by alternating key/value pairs (alist-style); (5) log levels are integers — CRITICAL(-1) through VERBOSE(4); lower numbers are more severe.") + ("tags" "log" "deflogger" "logging" "logger" + "start-system-logger" "v0.19") + ("title" . "v0.19: Structured logging with deflogger")) + (("code" + . + ";; let-hash rebinds %%ref so dot-prefixed identifiers look up the hash\n(let (ht (hash (name \"Alice\") (age 30) (role 'admin)))\n (let-hash ht\n (displayln .name) ; => Alice (hash-ref ht 'name)\n (displayln .?age) ; => 30 (hash-get ht 'age) weak — #f if missing\n (displayln .?missing))) ; => #f\n\n;; Dot syntax variants inside let-hash:\n;; .x -> (hash-ref ht 'x) strong: raises error if missing\n;; .?x -> (hash-get ht 'x) weak: returns #f if missing\n;; .$x -> (hash-get ht \"x\") string key, weak\n;; ..x -> escape: look up 'x in outer scope (not the hash)\n\n;; Nested hashes\n(let (ht (hash (user (hash (name \"Bob\") (id 7)))))\n (let-hash ht\n (let-hash .user\n (displayln .name \" #\" .id))))\n\n;; hash=? for structural equality\n(def a (hash (x 1) (y 2)))\n(def b (hash (x 1) (y 2)))\n(hash=? a b) ; => #t\n(equal-hash? a b) ; => #t (also checks hash-table?)") ("id" . "v019-hash-let-hash") ("imports" ":std/hash") + ("notes" + . + "let-hash is now in :std/hash (v0.19) — it was previously in :std/sugar (now removed). The .? prefix is the safe nil-returning variant. The .$ prefix looks up by string key. Use ..x to \"escape\" dot lookup and reference a binding from the outer scope. hash=? does deep key-value equality; equal-hash? additionally checks that both are hash-table? first.") + ("tags" "hash" "let-hash" "destructuring" "hash-ref" + "std/hash" "v0.19") + ("title" . "v0.19: let-hash for hash table destructuring")) + (("code" + . + ";; DELETED in v0.19 (no replacement needed or merged elsewhere)\n;;\n;; :std/sugar -> removed; let-hash moved to :std/hash\n;; awhen/chain/is/with-id still in core sugar\n;; :std/contract -> removed (was already a compat shim in v0.18)\n;; :std/misc/list -> functionality split across std/list/*\n;; :std/misc/hash -> :std/hash\n;; :std/misc/barrier -> :std/sync/barrier\n;; :std/misc/channel -> :std/sync/channel\n;; :std/misc/completion -> :std/sync/completion\n;; :std/misc/deque -> :std/struct/queue (or std/list/deque)\n;; :std/misc/ports -> :std/io/*\n;; :std/misc/threads -> removed (use :gerbil/runtime/thread)\n;; :std/misc/timeout -> :std/time/timeout\n;; :std/misc/walist -> :std/list/walist\n;; :std/logger -> :std/log (completely rewritten)\n;; :std/sort -> removed (use :std/srfi/132 or sort from srfi-32)\n;; :std/srfi/* -> all SRFIs removed from stdlib\n;; :std/actor-v13/* -> removed\n;; :std/actor-v18/* -> removed (replaced by new actor system)\n;; :std/format -> :std/format/* (new, C-printf style)\n;; :std/generic -> removed (use interface system)\n;; :std/io/strio/* -> :std/io/bio/* (buffered I/O rewrite)\n\n;; NEW in v0.19\n;; :std/iter -> completely rewritten (Iterator interface protocol)\n;; :std/format -> new C-printf style format module\n;; :std/log -> new structured logging (replaces :std/logger)\n;; :std/cache -> object/global cache management\n;; :std/serde -> serialization/deserialization framework\n;; :std/ffi -> FFI macrology (replaces :std/foreign)\n;; :std/hash -> hash utilities (let-hash, hash=?)\n;; :std/sync/spinlock -> new spinlock primitive\n;; :std/sync/barrier -> moved from :std/misc/barrier\n;; :std/sync/channel -> moved from :std/misc/channel\n;; :std/sync/completion -> moved from :std/misc/completion\n;; :std/sync/rwlock -> new reader-writer lock\n;; :std/list/walist -> moved from :std/misc/walist\n;; :std/time/timeout -> moved from :std/misc/timeout\n;; :std/time/time -> time utilities\n;; :std/time/format -> time formatting\n;; :std/net/address/* -> net address utilities\n\n;; Quick migration pattern for let-hash\n;; v0.18: (import :std/sugar)\n;; v0.19: (import :std/hash)\n(import :std/hash) ; let-hash is here now") ("id" . "v019-stdlib-migration") ("imports") + ("notes" + . + "v0.19 is a major stdlib reorganization. The :std/misc/* namespace is almost entirely removed — its contents moved to more specific top-level packages. The SRFI collection (:std/srfi/*) is removed. The generic dispatch system (:std/generic) is removed in favor of the interface system. The old iterator struct-based :std/iter is rewritten around interfaces. :std/sugar is removed — most sugar is now in gerbil/core or specific modules.") + ("tags" "v0.19" "migration" "std/misc" "sugar" "contract" + "stdlib") + ("title" + . + "v0.19: stdlib reorganization — what moved where")) + (("code" + . + ";; for iterates bindings IN PARALLEL (stops at shortest)\n(for ((x '(1 2 3)) (y '(a b c)))\n (displayln x \" \" y))\n;; 1 a\n;; 2 b\n;; 3 c\n\n;; for* iterates bindings NESTED (cross-product, inner resets each outer step)\n(for* ((x '(1 2 3)) (y '(a b)))\n (displayln x \" \" y))\n;; 1 a\n;; 1 b\n;; 2 a\n;; 2 b\n;; 3 a\n;; 3 b\n\n;; for* with for/collect-like pattern: use for/collect with nested for\n(for/collect (x '(1 2 3))\n (for/collect (y '(a b))\n (cons x y)))\n;; => (((1 . a) (1 . b)) ((2 . a) (2 . b)) ((3 . a) (3 . b)))\n\n;; for* with filter\n(for* ((x (in-range 3)) (y (in-range 3)) when (not (= x y)))\n (displayln x \",\" y))") ("id" . "v019-iter-for-star") ("imports" ":std/iter") + ("notes" + . + "for = parallel iteration (like Racket's for, Python zip). for* = nested/cross-product iteration (like Racket's for*, Python nested loops). for stops at the shortest iterator; for* exhausts all combinations. When you want a flat cross-product list, use for/collect with a for* body or nest for/collect calls.") + ("tags" "iter" "for*" "nested" "cross-product" "iteration" + "v0.19") + ("title" + . + "v0.19: for* for nested (cross-product) iteration")) + (("code" + . + ";; ObjectCache pools reusable objects to avoid repeated allocation\n;; (e.g., byte buffers, connection objects, scratch vectors)\n\n;; Create a named object cache\n(def buf-cache\n (ObjectCache\n name: '/my/buf-cache\n lock: (SpinLock)\n objects: []\n size: 0\n max-size: 32 ; hold up to 32 buffers\n new: (lambda () (make-u8vector 4096 0)) ; allocate fresh\n reset!: (lambda (buf) (u8vector-fill! buf 0)))) ; reset before reuse\n\n;; Register with the global cache registry (optional, for flush/stats)\n(global-cache-register! buf-cache)\n\n;; Get an object (from pool, or freshly allocated if empty)\n(let (buf (object-cache-get buf-cache))\n ;; ... use buf ...\n ;; Return it when done\n (object-cache-put! buf-cache buf))\n\n;; Global cache stats and flushing\n(global-cache-size) ; total objects across all registered caches\n(global-cache-flush!) ; release all pooled objects\n\n;; Flush a specific cache\n(object-cache-flush! buf-cache)") ("id" . "v019-cache-object") + ("imports" ":std/cache" ":std/iter") + ("notes" + . + "ObjectCache is a spin-lock protected pool. object-cache-get either pops from the pool or calls new:. object-cache-put! calls reset!: then pushes back if below max-size, otherwise discards. The global cache registry (global-cache-register!) is optional but lets you flush all caches at once (e.g., on memory pressure). SpinLock from :std/sync/spinlock is the recommended lock for object caches.") + ("tags" "cache" "object-cache" "pool" "ObjectCache" + "std/cache" "v0.19") + ("title" + . + "v0.19: Object cache for pooling reusable objects")) + (("code" + . + ";; std/ffi provides high-level macrology for C FFI (replaces :std/foreign)\n;; Requires compilation target C: (require ,(compilation-target? C))\n\n(require ,(compilation-target? C))\n(import :std/ffi)\n\n;; C-ffi-macrology: declare helper C macros (call once at top of file)\n(C-ffi-macrology)\n\n;; C-include: emit #include directives\n(C-include \"<stdio.h>\" \"<string.h>\")\n\n;; C-declare: emit raw C code\n(C-declare \"static int my_counter = 0;\")\n\n;; def-C: define a Scheme wrapper around a C expression\n;; Syntax: (def-C (proc-name (arg ~ :type) ...) => :return-type \"C code\")\n;; In the C code string, use $1, $2, ... for the (unwrapped) args\n(def-C (add-fixnums (a :- :fixnum) (b :- :fixnum)) => :fixnum\n \"$1 + $2\")\n\n(def-C (copy-bytes (dst :- :u8vector) (src :- :u8vector) (n :- :fixnum)) => :fixnum\n \"(memcpy($1, $2, $3), $3)\")\n\n;; Use the defined procedures normally\n(add-fixnums 3 5) ; => 8\n\n;; Type annotations in def-C:\n;; :fixnum -> int, unwrapped with ___INT(), wrapped with ___FIX()\n;; :flonum -> double, unwrapped with ___F64UNBOX()\n;; :u8vector -> __uint8_t*, unwrapped with ___U8VECTOR_AS()\n;; The => :return-type determines how to box the C return value") ("id" . "v019-ffi-def-c") ("imports" ":std/ffi") + ("notes" + . + "std/ffi is a v0.19 replacement for :std/foreign. def-C generates a Gambit ___BEGIN_C_LINKAGE block with inline C code plus the Scheme wrapper. The C code receives raw Gambit object pointers named ___ARG1, ___ARG2, ... which def-C unwraps to the correct C types per the :type annotations before passing as $1, $2, .... Use C-ffi-macrology once per compilation unit to get ___U8VECTOR_AS and ___TRAP_ERRNO helpers. The require guard ensures the form is only compiled for C-target builds.") + ("tags" "ffi" "C" "def-C" "C-include" "foreign" "v0.19") + ("title" + . + "v0.19: FFI macrology with def-C and C-include")) + (("code" + . + ";; support/build-check.ss\n#!chezscheme\n;;; Import-only build entrypoint: compile the same modules as main.ss,\n;;; but do not call cli-main or start the app.\n(import (chezscheme)\n (myapp core config)\n (myapp core agent)\n (myapp ui cli))\n\n;; Makefile\nJERBUILD ?= jerbuild\nJH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null)\nLIBDIRS := --libdirs ./lib:$(JH)/lib\n\nbuild:\n\t$(JERBUILD) compile $(LIBDIRS) support/build-check.ss\n") + ("id" . "jerbuild-import-only-build-check") ("imports") + ("notes" + . + "jerbuild compile executes the script after compiling it. If main.ss calls cli-main, a build target can accidentally start the TUI/CLI or hang. Use an import-only entrypoint that imports the same top-level modules needed to force compilation, but performs no application action.") + ("tags" "jerbuild" "compile" "entrypoint" "import" + "build-check" "cli") + ("title" + . + "Compile a Jerboa project without running its CLI entrypoint")) + (("code" + . + ";; support/sqlite-bundled/Cargo.toml\n[package]\nname = \"app-sqlite-bundled\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\ncrate-type = [\"staticlib\"]\n\n[dependencies]\nlibsqlite3-sys = { version = \"0.30\", features = [\"bundled\"] }\n\n;; support/sqlite-bundled/src/lib.rs\n#[no_mangle]\npub extern \"C\" fn app_sqlite_bundled_anchor() -> i32 {\n unsafe { libsqlite3_sys::sqlite3_libversion_number() }\n}\n\n;; .jerbuild\n(entry \"main.ss\")\n(output \"app\")\n(ffi-symbols \"support/ffi-symbols.list\")\n(extra-sources\n (\"vendor/jerboa-sqlite/jerboa_sqlite_shim.c\" cflags: \"-Isupport\"))\n(extra-archives\n \"support/sqlite-bundled/target/release/libapp_sqlite_bundled.a\")\n\n;; Makefile\nsupport/sqlite-bundled/target/release/libapp_sqlite_bundled.a:\n\tcargo build --manifest-path support/sqlite-bundled/Cargo.toml --release\n\nbinary: support/sqlite-bundled/target/release/libapp_sqlite_bundled.a\n\tjerbuild build --config .jerbuild --os-libs \"-lm -ldl -lpthread -luuid -lncurses -lstdc++\"\n") + ("id" . "jerbuild-bundled-sqlite-shim") ("imports") + ("notes" + . + "This keeps user repos buildable with jerbuild plus a C compiler and Rust/Cargo, without requiring system sqlite headers or libsqlite3 packages. Put a minimal sqlite3.h in support/ if the shim only needs a small API surface, add -Isupport on the shim source, and list the bundled Rust static archive in extra-archives so sqlite symbols are available at final link.") + ("tags" "jerbuild" "sqlite" "ffi" "staticlib" + "extra-archives" "c-shim") ("title" . - "Export compatibility aliases with R6RS rename"))) + "Bundle SQLite for a jerbuild binary with a Rust static archive and C shim"))) --- a/data/features.sexp +++ b/data/features.sexp @@ -824,7 +824,7 @@ ("use_case" . "Every /save-discoveries invocation. Every manual howto_add / suggest_feature / vote_feature / security_pattern_add. Anything that needs to persist discoveries back to the .sexp data files.") - ("votes" . 0)) + ("votes" . 1)) (("description" . "When `jerbuild binary` compiles a program importing (std net tls-rustls) or (std wasm sandbox), the resulting binary still needs libjerboa_native.dylib reachable at runtime via DYLD_LIBRARY_PATH/LD_LIBRARY_PATH, because those std modules do a NAMED (load-shared-object \"libjerboa_native.dylib\"). Add a flag (e.g. --static-native / --with-libjerboa-native) that links the prebuilt libjerboa_native.a via --extra-archive AND auto-emits the ffi-symbols registration header (Sforeign_symbol) for the archive's exported symbols, producing a self-contained binary needing no runtime dylib or DYLD_* env. jerbuild already supports --extra-archive plus a manual (ffi-symbols ...) mechanism; this packages the common case (the shipped libjerboa_native) behind one flag and auto-derives the symbol list (e.g. via nm) so the user need not enumerate jerboa_tls_*/jerboa_wasm_* by hand.") @@ -982,4 +982,78 @@ ("use_case" . "Validating large Jerboa modules after a small edit without falling back to a full make build.") + ("votes" . 0)) + (("description" + . + "When running jerboa_eval, there is no way to query which Gerbil version the runtime is using. Trying (gerbil-version-string) throws 'unbound identifier'. This makes it impossible to confirm whether v0.19 features are available before testing, or to include version context in error messages.") + ("estimated_token_reduction" + . + "~300 tokens per session: eliminates 3-4 failed eval probes plus the bash fallback to determine version") + ("example_scenario" + . + "Trying to test v0.19 std/iter patterns: (for (x (in-range 5)) body) fails because the runtime is v0.18.1. There was no way to discover this without trial-and-error. A (jerboa-runtime-version) or (##gerbil-version-string) binding in eval would let the recipe-authoring workflow confirm compatibility upfront.") + ("id" . "jerboa-version-query") ("impact" . "medium") + ("tags" "eval" "version" "runtime" "gerbil" "compatibility") + ("title" + . + "jerboa_eval: expose Gerbil runtime version string") + ("use_case" + . + "Discovering which stdlib API version is available before testing new patterns; writing version-conditional recipes; diagnosing why a function is missing.") + ("votes" . 0)) + (("description" + . + "When testing API patterns for a future Gerbil version (e.g., v0.19 while runtime is v0.18), jerboa_eval throws cryptic errors instead of a clear 'not available in this runtime' message. A gerbil_version parameter on jerboa_eval would let callers declare the minimum required version and get a clean skip/warning instead of a confusing exception.") + ("estimated_token_reduction" + . + "~400 tokens per session for version-crossing recipe work: eliminates confusing error interpretation and the bash version-probe detour") + ("example_scenario" + . + "Calling jerboa_eval to test (for (x (in-range 5)) body) from v0.19 std/iter while v0.18.1 is installed. The macro has different syntax in v0.18, so the error 'invalid syntax (x (in-range 5))' is confusing — it looks like the test code is wrong, not that it needs a newer runtime.") + ("id" . "jerboa-eval-imports-version-guard") + ("impact" . "medium") + ("tags" "eval" "version" "compatibility" "v0.19" "skip") + ("title" + . + "jerboa_eval: accept gerbil_version guard to skip incompatible evals") + ("use_case" + . + "Writing cookbook recipes for a not-yet-installed Gerbil version. Currently requires trying the call, getting an error, then manually noting 'needs v0.19'. A version guard would produce a clear message instead.") + ("votes" . 0)) + (("description" + . + "When exploring unfamiliar modules (especially checking if an installed module is the Jerboa version vs. the gerbil-origin version), there's no way to know which file jerboa_module_exports is reading. This session required a bash glob to find that std/iter was at /opt/gerbil/v0.18.1-173-gb3417266/lib/std/iter.ssi before understanding why the API didn't match expectations.") + ("estimated_token_reduction" + . + "~500 tokens: eliminates the bash glob + read .ssi file detour needed to identify which module version is active") + ("example_scenario" + . + "jerboa_module_exports for std/iter returned symbols from Jerboa's own std/iter (in-list, in-vector, in-hash-pairs...) not gerbil-origin's (iterator, iter-end, in-iota...). Without knowing the source path, it was impossible to understand why.") + ("id" . "jerboa-module-source-path") ("impact" . "medium") + ("tags" "module" "exports" "source" "path" "debugging") + ("title" + . + "jerboa_module_exports: include source file path in output") + ("use_case" + . + "Understanding which installed copy of a module is being used when multiple gerbil versions or Jerboa overrides are present. Critical for v0.18 vs v0.19 disambiguation.") + ("votes" . 0)) + (("description" + . + "When jmcp writer tools fail, the response can be the literal template 'Tool error: failed for ~a: ~(~a~)' instead of the tool name and exception text. This hides the actionable cause, such as invoking jmcp outside the repo without JERBOA_MCP_REPO set.") + ("estimated_token_reduction" + . + "~500-1500 tokens per failing MCP call; avoids source spelunking to infer the exception.") + ("example_scenario" + . + "Calling jerboa_howto_add from /Users/user/mine/jerboa-shell failed with 'Tool error: failed for ~a: ~(~a~)' because repo-root defaulted to the current directory and the data directory was not present. The real exception was hidden.") + ("id" . "fix-mcp-tool-error-formatting") + ("impact" . "medium") + ("tags" "mcp" "errors" "debugging" "format" "jmcp") + ("title" + . + "Render real exception details in MCP tool error responses") + ("use_case" + . + "Debugging failed MCP tool calls, especially cookbook/feature/security writer calls from direct JSON-RPC or the Claude MCP shim.") ("votes" . 0))) --- a/data/security-rules.sexp +++ b/data/security-rules.sexp @@ -893,4 +893,22 @@ . "\\(string=\\?\\s+[^\\)]*secret[^\\)]*\\s+\"\"\\)") ("scope" . "scheme") - ("severity" . "high"))) + ("severity" . "high")) + (("id" . "ffi-c-declare-string-injection") + ("message" + . + "C-declare and begin-foreign accept raw C code strings. If the string is constructed at runtime (e.g., via string-append or format), an attacker controlling any input segment can inject arbitrary C declarations, define malicious macros, or corrupt the generated C file.") + ("pattern" + . + "C-declare\\s+[^\"]*\\(string-append|begin-foreign[^)]*\\(string-append") + ("scope" . "ffi-boundary") + ("severity" . "high")) + (("id" . "ffi-def-c-u8vector-length-unchecked") + ("message" + . + "def-C with :u8vector arguments passes the raw byte pointer to C but does NOT automatically pass the vector's length. If the C code (memcpy, memmove, etc.) uses a caller-supplied length without verifying it against the actual u8vector size, a heap buffer overflow results.") + ("pattern" + . + "def-C.*:u8vector.*memcpy|def-C.*:u8vector.*memmove|def-C.*:u8vector.*memset") + ("scope" . "ffi-boundary") + ("severity" . "medium")))