updates
ober
f795ade199b0f5299b2b1746742f528a8f6a2bb0
--- a/data/api-signatures.sexp +++ b/data/api-signatures.sexp @@ -2,8 +2,8 @@ ("modules" ("(jerboa build musl)" ("exports" "build-musl-binary" "make-musl-cross-target" - "musl-available?" "musl-boot-files" "musl-chez-lib-dir" - "musl-chez-prefix" "musl-chez-prefix-set!" + "musl-available?" "musl-boot-files" "musl-jerboa-lib-dir" + "musl-jerboa-prefix" "musl-jerboa-prefix-set!" "musl-cross-available?" "musl-crt-objects" "musl-gcc-path" "musl-libkernel-path" "musl-link-command" "musl-sysroot" "validate-musl-setup") @@ -709,7 +709,7 @@ ("file" . "lib/jerboa/typed/rust.ss") ("tier" . "core")) ("(jerboa typed wrapper)" - ("exports" "abi-chez-type" + ("exports" "abi-jerboa-type" "typed-library-form->jerboa-wrapper-library-string" "typed-library-form->jerboa-wrapper-string" "typed-library-forms->jerboa-wrapper-files" @@ -7567,7 +7567,7 @@ ("WSTOPSIG" "(std os posix)") ("WTERMSIG" "(std os posix)") ("WUNTRACED" "(std os posix)") ("W_OK" "(std os posix)") ("X_OK" "(std os posix)") ("\\x7C" "(std pipeline)") - ("abi-chez-type" "(jerboa typed wrapper)") + ("abi-jerboa-type" "(jerboa typed wrapper)") ("abi-name" "(jerboa cross)") ("abi-safe-def?" "(jerboa typed rust)") ("abi-safe-type?" "(jerboa typed rust)") @@ -13421,9 +13421,9 @@ ("munmap" "(std mmap)" "(std os mmap)") ("musl-available?" "(jerboa build musl)") ("musl-boot-files" "(jerboa build musl)") - ("musl-chez-lib-dir" "(jerboa build musl)") - ("musl-chez-prefix" "(jerboa build musl)") - ("musl-chez-prefix-set!" "(jerboa build musl)") + ("musl-jerboa-lib-dir" "(jerboa build musl)") + ("musl-jerboa-prefix" "(jerboa build musl)") + ("musl-jerboa-prefix-set!" "(jerboa build musl)") ("musl-cross-available?" "(jerboa build musl)") ("musl-crt-objects" "(jerboa build musl)") ("musl-gcc-path" "(jerboa build musl)") --- a/data/cookbooks.sexp +++ b/data/cookbooks.sexp @@ -365,7 +365,7 @@ ("title" . "Defining R6RS Library Modules")) (("code" . - "(define c-errno-location (foreign-procedure \"__errno_location\" () void*))\n(define (get-errno) (foreign-ref 'int (c-errno-location) 0))\n(define EINTR 4)\n\n(define c-read (foreign-procedure \"read\" (int void* size_t) ssize_t))\n(define c-write (foreign-procedure \"write\" (int void* size_t) ssize_t))\n(define c-close (foreign-procedure \"close\" (int) int))\n\n(define (fd->ports fd name)\n ;; IMPORTANT: Both ports share the same fd.\n ;; A closed? flag prevents double-close when:\n ;; 1. close-port is called explicitly, AND\n ;; 2. Chez GC finalizes the port and calls the close handler again.\n ;; Without this flag, the second c-close(fd) closes a reused fd!\n (let ([closed? #f])\n (let ([in (make-custom-binary-input-port\n (string-append name \"-in\")\n (lambda (bv start count)\n (if closed? 0\n (let ([buf (make-bytevector count)])\n (let retry ()\n (let ([n (c-read fd buf count)])\n (cond\n [(> n 0) (bytevector-copy! buf 0 bv start n) n]\n [(and (< n 0) (= (get-errno) EINTR)) (retry)]\n [else 0]))))))\n #f #f\n (lambda ()\n (unless closed?\n (set! closed? #t)\n (c-close fd))))]\n [out (make-custom-binary-output-port\n (string-append name \"-out\")\n (lambda (bv start count)\n (if closed? 0\n (let ([buf (make-bytevector count)])\n (bytevector-copy! bv start buf 0 count)\n (let lp ([written 0])\n (if (= written count) count\n (let ([n (c-write fd\n (let ([tmp (make-bytevector (- count written))])\n (bytevector-copy! buf written tmp 0 (- count written))\n tmp)\n (- count written))])\n (cond\n [(> n 0) (lp (+ written n))]\n [(and (< n 0) (= (get-errno) EINTR)) (lp written)]\n [else written])))))))\n #f #f #f)]) ;; no close handler on out — fd closed by in only\n (values\n (transcoded-port in (make-transcoder (utf-8-codec) (eol-style none) (error-handling-mode replace)))\n (transcoded-port out (make-transcoder (utf-8-codec) (eol-style none) (error-handling-mode replace)))))))") ("id" . "chez-tcp-fd-ports") ("imports" "(chezscheme)") + "(define c-errno-location (foreign-procedure \"__errno_location\" () void*))\n(define (get-errno) (foreign-ref 'int (c-errno-location) 0))\n(define EINTR 4)\n\n(define c-read (foreign-procedure \"read\" (int void* size_t) ssize_t))\n(define c-write (foreign-procedure \"write\" (int void* size_t) ssize_t))\n(define c-close (foreign-procedure \"close\" (int) int))\n\n(define (fd->ports fd name)\n ;; IMPORTANT: Both ports share the same fd.\n ;; A closed? flag prevents double-close when:\n ;; 1. close-port is called explicitly, AND\n ;; 2. Chez GC finalizes the port and calls the close handler again.\n ;; Without this flag, the second c-close(fd) closes a reused fd!\n (let ([closed? #f])\n (let ([in (make-custom-binary-input-port\n (string-append name \"-in\")\n (lambda (bv start count)\n (if closed? 0\n (let ([buf (make-bytevector count)])\n (let retry ()\n (let ([n (c-read fd buf count)])\n (cond\n [(> n 0) (bytevector-copy! buf 0 bv start n) n]\n [(and (< n 0) (= (get-errno) EINTR)) (retry)]\n [else 0]))))))\n #f #f\n (lambda ()\n (unless closed?\n (set! closed? #t)\n (c-close fd))))]\n [out (make-custom-binary-output-port\n (string-append name \"-out\")\n (lambda (bv start count)\n (if closed? 0\n (let ([buf (make-bytevector count)])\n (bytevector-copy! bv start buf 0 count)\n (let lp ([written 0])\n (if (= written count) count\n (let ([n (c-write fd\n (let ([tmp (make-bytevector (- count written))])\n (bytevector-copy! buf written tmp 0 (- count written))\n tmp)\n (- count written))])\n (cond\n [(> n 0) (lp (+ written n))]\n [(and (< n 0) (= (get-errno) EINTR)) (lp written)]\n [else written])))))))\n #f #f #f)]) ;; no close handler on out — fd closed by in only\n (values\n (transcoded-port in (make-transcoder (utf-8-codec) (eol-style none) (error-handling-mode replace)))\n (transcoded-port out (make-transcoder (utf-8-codec) (eol-style none) (error-handling-mode replace)))))))") ("id" . "jerboa-tcp-fd-ports") ("imports" "(chezscheme)") ("notes" . "Two bugs to avoid: (1) Chez GC calls the close handler of custom ports when they're collected, even after close-port was already called explicitly. The closed? flag prevents c-close from being called twice on the same fd (which would close a reused fd). (2) Chez stop-the-world GC sends SIGURG/signals to interrupt blocking syscalls, causing EINTR. Always retry on EINTR in c-read and c-write. Only the input port needs a close handler — output port uses #f to avoid double-close.") @@ -376,7 +376,7 @@ "TCP fd->ports with closed? flag and EINTR retry")) (("code" . - "(define c-errno-location (foreign-procedure \"__errno_location\" () void*))\n(define (get-errno) (foreign-ref 'int (c-errno-location) 0))\n(define EINTR 4)\n\n;; tcp-accept with EINTR retry\n(define (tcp-accept srv)\n (let loop ()\n (let ([client-fd (c-accept (tcp-server-fd srv) 0 0)])\n (cond\n [(>= client-fd 0) (fd->ports client-fd \"tcp-client\")]\n [(= (get-errno) EINTR) (loop)]\n [else (error 'tcp-accept \"accept() failed\")]))))\n\n;; tcp-connect with EINTR retry\n(define (tcp-connect address port)\n (let ([fd (c-socket AF_INET SOCK_STREAM 0)])\n (when (< fd 0) (error 'tcp-connect \"socket() failed\"))\n (let ([addr (make-sockaddr-in address port)])\n (let loop ()\n (let ([rc (c-connect fd addr SOCKADDR_IN_SIZE)])\n (cond\n [(>= rc 0)\n (foreign-free addr)\n (fd->ports fd \"tcp-connection\")]\n [(= (get-errno) EINTR) (loop)]\n [else\n (foreign-free addr)\n (c-close fd)\n (error 'tcp-connect \"connect() failed\" address port)]))))))") ("id" . "chez-tcp-accept-eintr-retry") + "(define c-errno-location (foreign-procedure \"__errno_location\" () void*))\n(define (get-errno) (foreign-ref 'int (c-errno-location) 0))\n(define EINTR 4)\n\n;; tcp-accept with EINTR retry\n(define (tcp-accept srv)\n (let loop ()\n (let ([client-fd (c-accept (tcp-server-fd srv) 0 0)])\n (cond\n [(>= client-fd 0) (fd->ports client-fd \"tcp-client\")]\n [(= (get-errno) EINTR) (loop)]\n [else (error 'tcp-accept \"accept() failed\")]))))\n\n;; tcp-connect with EINTR retry\n(define (tcp-connect address port)\n (let ([fd (c-socket AF_INET SOCK_STREAM 0)])\n (when (< fd 0) (error 'tcp-connect \"socket() failed\"))\n (let ([addr (make-sockaddr-in address port)])\n (let loop ()\n (let ([rc (c-connect fd addr SOCKADDR_IN_SIZE)])\n (cond\n [(>= rc 0)\n (foreign-free addr)\n (fd->ports fd \"tcp-connection\")]\n [(= (get-errno) EINTR) (loop)]\n [else\n (foreign-free addr)\n (c-close fd)\n (error 'tcp-connect \"connect() failed\" address port)]))))))") ("id" . "jerboa-tcp-accept-eintr-retry") ("imports" "(chezscheme)") ("notes" . @@ -386,7 +386,7 @@ ("title" . "tcp-accept and tcp-connect with EINTR retry")) (("code" . - "(define c-errno-location\n (foreign-procedure \"__errno_location\" () void*))\n\n(define (get-errno)\n (foreign-ref 'int (c-errno-location) 0))\n\n;; Common errno values (Linux)\n(define EINTR 4)\n(define EBADF 9)\n(define EAGAIN 11)\n(define EPIPE 32)\n\n;; Usage: distinguish real errors from transient ones\n(let ([n (c-read fd buf count)])\n (cond\n [(> n 0) n]\n [(and (< n 0) (= (get-errno) EINTR)) 'interrupted]\n [else (error 'c-read \"read failed\" (get-errno))]))") ("id" . "chez-errno-access") ("imports" "(chezscheme)") + "(define c-errno-location\n (foreign-procedure \"__errno_location\" () void*))\n\n(define (get-errno)\n (foreign-ref 'int (c-errno-location) 0))\n\n;; Common errno values (Linux)\n(define EINTR 4)\n(define EBADF 9)\n(define EAGAIN 11)\n(define EPIPE 32)\n\n;; Usage: distinguish real errors from transient ones\n(let ([n (c-read fd buf count)])\n (cond\n [(> n 0) n]\n [(and (< n 0) (= (get-errno) EINTR)) 'interrupted]\n [else (error 'c-read \"read failed\" (get-errno))]))") ("id" . "jerboa-errno-access") ("imports" "(chezscheme)") ("notes" . "DEPRECATED: Use (std os errno) instead — see recipe std-os-errno-cross-platform. That module handles Linux, macOS, and FreeBSD automatically, provides named constants (EINTR, EAGAIN, etc.) with correct platform values, and exposes errno, errno-strerror, errno-name, and errno-supported?. This raw FFI approach hard-codes __errno_location which is Linux glibc only; it will fail silently on macOS (__error) and musl (__errno). Kept here for reference only.") @@ -396,7 +396,7 @@ ("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" . "chez-script-mode-threads") + ";;; 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") ("imports" "(chezscheme)") ("notes" . @@ -408,7 +408,8 @@ "Chez --script mode: forked threads don't 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" . "chez-collect-deadlock") ("imports" "(chezscheme)") + ";;; 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") + ("imports" "(chezscheme)") ("notes" . "Chez (collect) is a stop-the-world GC that requires ALL threads to reach a safe point before proceeding. Threads blocked in foreign calls (c-read, c-accept, c-connect) cannot reach a safe point until the foreign call returns. If the foreign call is waiting for data that the calling thread would provide after GC completes, you have a deadlock. Do not call (collect) explicitly in code that may run while other threads are in blocking I/O. Report bytes-allocated instead, or document the limitation.") @@ -419,7 +420,7 @@ "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" . "chez-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 --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") ("imports" "(chezscheme)") ("notes" . @@ -431,7 +432,7 @@ "Chez void is 0-arg; use (lambda _ (void)) as error handler")) (("code" . - "(import (except (chezscheme) make-hash-table hash-table? iota 1+ 1-)\n (std net tcp))\n\n(define (sleep-ms ms)\n (sleep (make-time 'time-duration (* ms 1000000) 0)))\n\n;;; Use fresh server per test group to:\n;;; 1. Limit total connections per server instance (avoids fd-reuse bugs)\n;;; 2. Test start/stop lifecycle separately\n;;; 3. Get clean state for each group\n\n;; Helper: run body with fresh debug-repl server, guaranteed cleanup\n;; NOTE: guard can interfere with threads; use simple let if issues arise\n(define-syntax with-fresh-tcp-server\n (syntax-rules ()\n ((_ start-fn stop-fn (port-var) body ...)\n (let ((port-var (start-fn)))\n (sleep-ms 50) ;; let accept thread start\n (let ((result (begin body ...)))\n (stop-fn)\n (sleep-ms 100) ;; let threads clean up\n result)))))\n\n;; Test group: each group gets its own server instance\n;; Server 1: basic connection test\n(let ((p (start-debug-repl! 0)))\n (sleep-ms 150)\n (let-values (((in out) (tcp-connect \"127.0.0.1\" p)))\n (get-line in) ;; banner\n (get-line in) ;; prompt\n (close-port in)\n (close-port out))\n (stop-debug-repl!)\n (sleep-ms 200))\n\n;; Server 2: command test (separate server!)\n(let ((p (start-debug-repl! 0)))\n (sleep-ms 150)\n (let-values (((in out) (tcp-connect \"127.0.0.1\" p)))\n ;; run multiple commands in ONE connection\n (get-line in) (get-line in)\n (put-string out \"(+ 1 2)\\n\") (flush-output-port out)\n (display (get-line in)) ;; \"3\"\n (close-port in)\n (close-port out))\n (stop-debug-repl!)\n (sleep-ms 200))") ("id" . "chez-tcp-server-test-pattern") + "(import (except (chezscheme) make-hash-table hash-table? iota 1+ 1-)\n (std net tcp))\n\n(define (sleep-ms ms)\n (sleep (make-time 'time-duration (* ms 1000000) 0)))\n\n;;; Use fresh server per test group to:\n;;; 1. Limit total connections per server instance (avoids fd-reuse bugs)\n;;; 2. Test start/stop lifecycle separately\n;;; 3. Get clean state for each group\n\n;; Helper: run body with fresh debug-repl server, guaranteed cleanup\n;; NOTE: guard can interfere with threads; use simple let if issues arise\n(define-syntax with-fresh-tcp-server\n (syntax-rules ()\n ((_ start-fn stop-fn (port-var) body ...)\n (let ((port-var (start-fn)))\n (sleep-ms 50) ;; let accept thread start\n (let ((result (begin body ...)))\n (stop-fn)\n (sleep-ms 100) ;; let threads clean up\n result)))))\n\n;; Test group: each group gets its own server instance\n;; Server 1: basic connection test\n(let ((p (start-debug-repl! 0)))\n (sleep-ms 150)\n (let-values (((in out) (tcp-connect \"127.0.0.1\" p)))\n (get-line in) ;; banner\n (get-line in) ;; prompt\n (close-port in)\n (close-port out))\n (stop-debug-repl!)\n (sleep-ms 200))\n\n;; Server 2: command test (separate server!)\n(let ((p (start-debug-repl! 0)))\n (sleep-ms 150)\n (let-values (((in out) (tcp-connect \"127.0.0.1\" p)))\n ;; run multiple commands in ONE connection\n (get-line in) (get-line in)\n (put-string out \"(+ 1 2)\\n\") (flush-output-port out)\n (display (get-line in)) ;; \"3\"\n (close-port in)\n (close-port out))\n (stop-debug-repl!)\n (sleep-ms 200))") ("id" . "jerboa-tcp-server-test-pattern") ("imports" "(chezscheme)" "(std net tcp)") ("notes" . @@ -443,7 +444,7 @@ "TCP server test pattern: fresh server per test group")) (("code" . - ";; compile-whole-program throws an exception when a .wpo is older than its .sls:\n;; \"does not define expected compilation instance of library (std sugar)\"\n;;\n;; The build continues past the exception but jsh-all.so is NOT generated,\n;; causing cascading failures (missing .h files, broken binary).\n;;\n;; FIX: Before compile-whole-program, scan all library dirs and recompile stale .wpo:\n(define (recompile-stale-wpo! wpo-path)\n (let* ([base (substring wpo-path 0 (- (string-length wpo-path) 4))]\n [sls (string-append base \".sls\")])\n (when (and (file-exists? sls)\n (time>? (file-modification-time sls) (file-modification-time wpo-path)))\n (printf \" Recompiling stale: ~a~n\" sls)\n (parameterize ([generate-wpo-files #t]\n [compile-imported-libraries #f]\n [optimize-level 2]\n [generate-inspector-information #f])\n (compile-library sls)))))\n\n;; Call this for every .wpo file in your library directories BEFORE\n;; calling compile-whole-program.") ("id" . "chez-stale-wpo-build-failure") + ";; compile-whole-program throws an exception when a .wpo is older than its .sls:\n;; \"does not define expected compilation instance of library (std sugar)\"\n;;\n;; The build continues past the exception but jsh-all.so is NOT generated,\n;; causing cascading failures (missing .h files, broken binary).\n;;\n;; FIX: Before compile-whole-program, scan all library dirs and recompile stale .wpo:\n(define (recompile-stale-wpo! wpo-path)\n (let* ([base (substring wpo-path 0 (- (string-length wpo-path) 4))]\n [sls (string-append base \".sls\")])\n (when (and (file-exists? sls)\n (time>? (file-modification-time sls) (file-modification-time wpo-path)))\n (printf \" Recompiling stale: ~a~n\" sls)\n (parameterize ([generate-wpo-files #t]\n [compile-imported-libraries #f]\n [optimize-level 2]\n [generate-inspector-information #f])\n (compile-library sls)))))\n\n;; Call this for every .wpo file in your library directories BEFORE\n;; calling compile-whole-program.") ("id" . "jerboa-stale-wpo-build-failure") ("imports" "(chezscheme)") ("notes" . @@ -455,7 +456,8 @@ "Stale .wpo files break compile-whole-program silently")) (("code" . - ";; WRONG — C-style single-string putenv:\n;; (putenv \"SSH_AUTH_SOCK=/tmp/agent.123\") ;; ERROR: wrong number of args\n\n;; RIGHT — Chez putenv takes name and value separately:\n(putenv \"SSH_AUTH_SOCK\" \"/tmp/agent.123\")\n\n;; NOTE: Chez does NOT export setenv. Use putenv instead.\n;; If you need POSIX setenv (e.g., for overwrite flag):\n(define c-setenv (foreign-procedure \"setenv\" (string string int) int))\n(c-setenv \"MY_VAR\" \"value\" 1) ;; 1 = overwrite") ("id" . "chez-putenv-two-args") ("imports" "(chezscheme)") + ";; WRONG — C-style single-string putenv:\n;; (putenv \"SSH_AUTH_SOCK=/tmp/agent.123\") ;; ERROR: wrong number of args\n\n;; RIGHT — Chez putenv takes name and value separately:\n(putenv \"SSH_AUTH_SOCK\" \"/tmp/agent.123\")\n\n;; NOTE: Chez does NOT export setenv. Use putenv instead.\n;; If you need POSIX setenv (e.g., for overwrite flag):\n(define c-setenv (foreign-procedure \"setenv\" (string string int) int))\n(c-setenv \"MY_VAR\" \"value\" 1) ;; 1 = overwrite") ("id" . "jerboa-putenv-two-args") + ("imports" "(chezscheme)") ("notes" . "Chez Scheme's putenv is NOT the C putenv(\"NAME=VALUE\"). It takes two arguments: (putenv name value). Chez does not export setenv — if you need it, use foreign-procedure to bind the C setenv. putenv updates Chez's internal environment table but does NOT update /proc/self/environ (which is frozen at process start).") @@ -465,7 +467,7 @@ "Chez putenv takes two args, not C-style single string")) (("code" . - ";; PROBLEM: After (putenv \"SSH_AUTH_SOCK\" path), child processes\n;; spawned by jsh don't see the variable because:\n;;\n;; 1. Chez's putenv updates Chez's INTERNAL env table\n;; 2. gambit-compat's (get-environment-variables) reads /proc/self/environ\n;; 3. /proc/self/environ is FROZEN at process start (Linux kernel)\n;; 4. jsh's fork-exec builds child env from get-environment-variables\n;;\n;; So putenv changes are invisible to the child env builder.\n;;\n;; FIX for jsh: Use jsh's own shell environment model:\n;; (run-cmd \"export SSH_AUTH_SOCK=/tmp/agent.123\")\n;;\n;; This sets the var in jsh's shell env (env-exported-alist),\n;; which IS used when building the child process environment.\n\n;; GENERAL FIX: If you control the fork-exec, use POSIX setenv\n;; via FFI, which updates the C environ array that execve inherits:\n(define c-setenv (foreign-procedure \"setenv\" (string string int) int))\n(c-setenv \"SSH_AUTH_SOCK\" \"/tmp/agent.123\" 1)") ("id" . "chez-putenv-proc-environ-frozen") + ";; PROBLEM: After (putenv \"SSH_AUTH_SOCK\" path), child processes\n;; spawned by jsh don't see the variable because:\n;;\n;; 1. Chez's putenv updates Chez's INTERNAL env table\n;; 2. gambit-compat's (get-environment-variables) reads /proc/self/environ\n;; 3. /proc/self/environ is FROZEN at process start (Linux kernel)\n;; 4. jsh's fork-exec builds child env from get-environment-variables\n;;\n;; So putenv changes are invisible to the child env builder.\n;;\n;; FIX for jsh: Use jsh's own shell environment model:\n;; (run-cmd \"export SSH_AUTH_SOCK=/tmp/agent.123\")\n;;\n;; This sets the var in jsh's shell env (env-exported-alist),\n;; which IS used when building the child process environment.\n\n;; GENERAL FIX: If you control the fork-exec, use POSIX setenv\n;; via FFI, which updates the C environ array that execve inherits:\n(define c-setenv (foreign-procedure \"setenv\" (string string int) int))\n(c-setenv \"SSH_AUTH_SOCK\" \"/tmp/agent.123\" 1)") ("id" . "jerboa-putenv-proc-environ-frozen") ("imports" "(chezscheme)") ("notes" . @@ -477,11 +479,11 @@ "putenv doesn't update /proc/self/environ — child env propagation")) (("code" . - ";; PROBLEM: (import (chez-ssh)) at compile time creates a hard dependency\n;; on OpenSSL symbols. musl static builds fail without OpenSSL.\n;;\n;; FIX: Load the library at runtime via eval + guard.\n;; The library must already be in the boot file or loadable.\n\n(define *ssh-available* #f)\n\n(define (ssh-available?)\n (unless *ssh-available*\n (set! *ssh-available*\n (guard (e [#t 'unavailable])\n (eval '(import (chez-ssh)) (interaction-environment))\n 'ready)))\n (eq? *ssh-available* 'ready))\n\n;; Call functions via eval:\n(define (ssh-call proc-name . args)\n (and (ssh-available?)\n (apply (eval proc-name (interaction-environment)) args)))\n\n;; Usage:\n(ssh-call 'ssh-agent-start)\n(ssh-call 'ssh-agent-load-key-file \"/path/to/key\")") ("id" . "chez-runtime-optional-import") + ";; PROBLEM: (import (jerboa-ssh)) at compile time creates a hard dependency\n;; on OpenSSL symbols. musl static builds fail without OpenSSL.\n;;\n;; FIX: Load the library at runtime via eval + guard.\n;; The library must already be in the boot file or loadable.\n\n(define *ssh-available* #f)\n\n(define (ssh-available?)\n (unless *ssh-available*\n (set! *ssh-available*\n (guard (e [#t 'unavailable])\n (eval '(import (jerboa-ssh)) (interaction-environment))\n 'ready)))\n (eq? *ssh-available* 'ready))\n\n;; Call functions via eval:\n(define (ssh-call proc-name . args)\n (and (ssh-available?)\n (apply (eval proc-name (interaction-environment)) args)))\n\n;; Usage:\n(ssh-call 'ssh-agent-start)\n(ssh-call 'ssh-agent-load-key-file \"/path/to/key\")") ("id" . "jerboa-runtime-optional-import") ("imports" "(chezscheme)") ("notes" . - "This pattern makes a library completely optional at runtime. The guard catches any import failure (missing .so, missing FFI symbols). The availability check runs once and caches the result. All function calls go through eval on the interaction-environment. This is used in jsh to make chez-ssh optional for musl builds that lack OpenSSL. The library .so must be in the boot file or on the library search path.") + "This pattern makes a library completely optional at runtime. The guard catches any import failure (missing .so, missing FFI symbols). The availability check runs once and caches the result. All function calls go through eval on the interaction-environment. This is used in jsh to make jerboa-ssh optional for musl builds that lack OpenSSL. The library .so must be in the boot file or on the library search path.") ("tags" "import" "eval" "optional" "runtime" "guard" "musl" "dynamic") ("title" @@ -489,7 +491,7 @@ "Runtime import via eval for optional library dependencies")) (("code" . - ";; PROBLEM: Binary works in dev but fails at runtime:\n;; \"Exception: library (std os landlock) not found\"\n;;\n;; CAUSE: make-boot-file only includes libraries you explicitly list.\n;; If module A imports (std os landlock) but you didn't add landlock.so\n;; to the boot file list, it fails at runtime.\n;;\n;; FIX: Add ALL transitive dependencies to the boot file:\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (map (lambda (m) (format \"~a/~a.so\" lib-dir m))\n '(\"jerboa/core\"\n \"jerboa/runtime\"\n \"std/error\"\n \"std/format\"\n ;; ... all direct imports ...\n \"std/os/landlock\" ;; <-- easy to miss!\n \"std/os/sandbox\" ;; <-- easy to miss!\n )))\n\n;; TIP: When you get \"library not found\" at runtime in a binary,\n;; add the missing .so to the boot file list in build-binary-*.ss") ("id" . "chez-boot-file-missing-library") + ";; PROBLEM: Binary works in dev but fails at runtime:\n;; \"Exception: library (std os landlock) not found\"\n;;\n;; CAUSE: make-boot-file only includes libraries you explicitly list.\n;; If module A imports (std os landlock) but you didn't add landlock.so\n;; to the boot file list, it fails at runtime.\n;;\n;; FIX: Add ALL transitive dependencies to the boot file:\n(apply make-boot-file \"jsh.boot\" '(\"scheme\" \"petite\")\n (map (lambda (m) (format \"~a/~a.so\" lib-dir m))\n '(\"jerboa/core\"\n \"jerboa/runtime\"\n \"std/error\"\n \"std/format\"\n ;; ... all direct imports ...\n \"std/os/landlock\" ;; <-- easy to miss!\n \"std/os/sandbox\" ;; <-- easy to miss!\n )))\n\n;; TIP: When you get \"library not found\" at runtime in a binary,\n;; add the missing .so to the boot file list in build-binary-*.ss") ("id" . "jerboa-boot-file-missing-library") ("imports" "(chezscheme)") ("notes" . @@ -501,7 +503,7 @@ "Boot file must include ALL transitive library dependencies")) (("code" . - ";; Chez Scheme's fork-thread (in c/thread.c) properly cleans up:\n;;\n;; start_thread() calls Scall0(cp) to run the thunk, then:\n;; destroy_thread(tc) — removes from S_threads list, decrements S_nthreads\n;; s_thread_setspecific(S_tc_key, NULL)\n;;\n;; So fork-thread does NOT leave phantom threads. S_nthreads is decremented\n;; when the thunk returns normally or via abort/exit handlers.\n;;\n;; The Gambit-compat wrapper (std misc thread) uses fork-thread internally:\n;; (define (thread-start! t)\n;; (fork-thread (lambda () ... (thunk) ...)))\n;;\n;; This means make-thread + thread-start! also clean up properly.\n;; You do NOT need to call thread-join! for cleanup — only for getting results.\n\n;; SAFE — thread cleans up automatically:\n(fork-thread (lambda () (do-background-work)))\n\n;; Also SAFE via Gambit compat layer:\n(thread-start! (make-thread (lambda () (do-background-work)) 'worker))") ("id" . "chez-smp-fork-thread-cleanup") ("imports") + ";; Chez Scheme's fork-thread (in c/thread.c) properly cleans up:\n;;\n;; start_thread() calls Scall0(cp) to run the thunk, then:\n;; destroy_thread(tc) — removes from S_threads list, decrements S_nthreads\n;; s_thread_setspecific(S_tc_key, NULL)\n;;\n;; So fork-thread does NOT leave phantom threads. S_nthreads is decremented\n;; when the thunk returns normally or via abort/exit handlers.\n;;\n;; The Gambit-compat wrapper (std misc thread) uses fork-thread internally:\n;; (define (thread-start! t)\n;; (fork-thread (lambda () ... (thunk) ...)))\n;;\n;; This means make-thread + thread-start! also clean up properly.\n;; You do NOT need to call thread-join! for cleanup — only for getting results.\n\n;; SAFE — thread cleans up automatically:\n(fork-thread (lambda () (do-background-work)))\n\n;; Also SAFE via Gambit compat layer:\n(thread-start! (make-thread (lambda () (do-background-work)) 'worker))") ("id" . "jerboa-smp-fork-thread-cleanup") ("imports") ("notes" . "Verified by reading ChezScheme/c/thread.c lines 332-365. S_fork_thread creates the thread, start_thread runs the thunk, then calls destroy_thread which decrements S_nthreads. No phantom threads. The earlier assumption that phantom threads caused GC deadlocks was wrong — the real issue is blocking the primordial thread's event loop with synchronous operations.") @@ -512,7 +514,7 @@ "Chez SMP: fork-thread properly decrements S_nthreads")) (("code" . - "/* C shim: EINTR-safe recv wrapper.\n * SIGCHLD (or any signal) can interrupt blocking recv/send/read,\n * returning -1 with errno==EINTR. Without a retry loop, the caller\n * interprets -1 as EOF or error and drops the connection.\n */\nint ffi_bv_recv(int fd, unsigned char *buf, int offset, int maxlen) {\n ssize_t n;\n do {\n n = recv(fd, buf + offset, maxlen, 0);\n } while (n < 0 && errno == EINTR);\n if (n < 0) return -1;\n return (int)n;\n}\n\nint ffi_bv_send(int fd, const unsigned char *buf, int offset, int len) {\n ssize_t n;\n do {\n n = send(fd, buf + offset, len, MSG_NOSIGNAL);\n } while (n < 0 && errno == EINTR);\n if (n < 0) return -errno;\n return (int)n;\n}\n\nint ffi_bv_read_nonblock(int fd, unsigned char *buf, int offset, int max_len) {\n ssize_t n;\n do {\n n = read(fd, buf + offset, max_len);\n } while (n < 0 && errno == EINTR);\n if (n < 0) {\n if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;\n return -1;\n }\n if (n == 0) return -1; /* EOF */\n return (int)n;\n}") ("id" . "chez-eintr-retry-c-shim") ("imports") + "/* C shim: EINTR-safe recv wrapper.\n * SIGCHLD (or any signal) can interrupt blocking recv/send/read,\n * returning -1 with errno==EINTR. Without a retry loop, the caller\n * interprets -1 as EOF or error and drops the connection.\n */\nint ffi_bv_recv(int fd, unsigned char *buf, int offset, int maxlen) {\n ssize_t n;\n do {\n n = recv(fd, buf + offset, maxlen, 0);\n } while (n < 0 && errno == EINTR);\n if (n < 0) return -1;\n return (int)n;\n}\n\nint ffi_bv_send(int fd, const unsigned char *buf, int offset, int len) {\n ssize_t n;\n do {\n n = send(fd, buf + offset, len, MSG_NOSIGNAL);\n } while (n < 0 && errno == EINTR);\n if (n < 0) return -errno;\n return (int)n;\n}\n\nint ffi_bv_read_nonblock(int fd, unsigned char *buf, int offset, int max_len) {\n ssize_t n;\n do {\n n = read(fd, buf + offset, max_len);\n } while (n < 0 && errno == EINTR);\n if (n < 0) {\n if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;\n return -1;\n }\n if (n == 0) return -1; /* EOF */\n return (int)n;\n}") ("id" . "jerboa-eintr-retry-c-shim") ("imports") ("notes" . "Root cause of mux client spontaneous disconnects: SIGCHLD from child processes interrupted recv() in the client relay loop. Without EINTR retry, recv returned -1, mux-read-message returned #f (EOF), and the relay loop exited. Always wrap blocking I/O syscalls in do/while EINTR loops in C FFI shims. MSG_NOSIGNAL on send prevents SIGPIPE from killing the process.") @@ -523,7 +525,7 @@ "EINTR retry loops in C FFI shims for recv/send/read")) (("code" . - "/* C shim: open /dev/null for any closed standard fds.\n * Call this at the start of a daemonized process.\n *\n * Problem: ffi_fork_exec closes ALL fds in the child before exec.\n * When the daemon later opens files/sockets, the kernel assigns\n * the lowest available fd numbers — which are 0, 1, 2. This causes\n * fd collisions: a socket gets fd 1 (stdout), and any Chez port\n * output goes to the socket instead of nowhere.\n */\nint ffi_ensure_std_fds(void) {\n int count = 0;\n for (int fd = 0; fd <= 2; fd++) {\n if (fcntl(fd, F_GETFD) == -1 && errno == EBADF) {\n int nfd = open(\"/dev/null\", (fd == 0) ? O_RDONLY : O_WRONLY);\n if (nfd >= 0 && nfd != fd) {\n dup2(nfd, fd);\n close(nfd);\n }\n count++;\n }\n }\n return count;\n}\n\n;; Scheme side:\n(define-foreign ffi-ensure-std-fds \"ffi_ensure_std_fds\" () -> int)\n\n;; Call at the start of your daemon entry point:\n(define (daemon-start)\n (ffi-ensure-std-fds) ; ensure 0/1/2 → /dev/null\n ...)") ("id" . "chez-daemon-fd-hygiene") ("imports") + "/* C shim: open /dev/null for any closed standard fds.\n * Call this at the start of a daemonized process.\n *\n * Problem: ffi_fork_exec closes ALL fds in the child before exec.\n * When the daemon later opens files/sockets, the kernel assigns\n * the lowest available fd numbers — which are 0, 1, 2. This causes\n * fd collisions: a socket gets fd 1 (stdout), and any Chez port\n * output goes to the socket instead of nowhere.\n */\nint ffi_ensure_std_fds(void) {\n int count = 0;\n for (int fd = 0; fd <= 2; fd++) {\n if (fcntl(fd, F_GETFD) == -1 && errno == EBADF) {\n int nfd = open(\"/dev/null\", (fd == 0) ? O_RDONLY : O_WRONLY);\n if (nfd >= 0 && nfd != fd) {\n dup2(nfd, fd);\n close(nfd);\n }\n count++;\n }\n }\n return count;\n}\n\n;; Scheme side:\n(define-foreign ffi-ensure-std-fds \"ffi_ensure_std_fds\" () -> int)\n\n;; Call at the start of your daemon entry point:\n(define (daemon-start)\n (ffi-ensure-std-fds) ; ensure 0/1/2 → /dev/null\n ...)") ("id" . "jerboa-daemon-fd-hygiene") ("imports") ("notes" . "Without this, the first socket opened by the daemon gets fd 0, 1, or 2. Chez Scheme's port system uses fds 0/1/2 for current-input-port, current-output-port, current-error-port. If a socket occupies fd 1, (display ...) writes to the socket. For musl static binaries, register the symbol in both extern declarations AND Sforeign_symbol calls in the build script.") @@ -534,7 +536,7 @@ "Ensure fds 0/1/2 are open in daemonized processes")) (("code" . - ";; Problem: ioctl(fd, TIOCGWINSZ, &ws) can succeed (return 0) but\n;; report ws_col=0 and ws_row=0. This happens with:\n;; - expect(1) default PTYs\n;; - PTYs allocated without stty rows/cols\n;; - Detached PTYs\n;;\n;; If you pass rows=0 to rendering code, (- rows 1) = -1, and\n;; (do ([r 0 (+ r 1)]) ((= r -1)) ...) loops forever.\n\n;; Fix 1: Guard at the protocol boundary (server-side resize handler)\n(define (handle-resize! client payload)\n (let ([raw-cols (bytevector-u16-ref payload 0 (endianness big))]\n [raw-rows (bytevector-u16-ref payload 2 (endianness big))])\n ;; Default 0x0 to 80x24\n (let ([cols (if (= raw-cols 0) 80 raw-cols)]\n [rows (if (= raw-rows 0) 24 raw-rows)])\n (client-cols-set! client cols)\n (client-rows-set! client rows))))\n\n;; Fix 2: Guard at the rendering boundary (defense in depth)\n(define (render-screen pane cols rows)\n (let ([cols (if (<= cols 0) 80 cols)]\n [rows (if (<= rows 0) 24 rows)])\n ;; Now (- rows 1) is always >= 0\n (let ([vt-rows (min (- rows 1) (vt-total-rows vt))])\n (do ([r 0 (+ r 1)])\n ((= r vt-rows))\n (render-row r)))))") ("id" . "chez-terminal-size-zero-guard") ("imports") + ";; Problem: ioctl(fd, TIOCGWINSZ, &ws) can succeed (return 0) but\n;; report ws_col=0 and ws_row=0. This happens with:\n;; - expect(1) default PTYs\n;; - PTYs allocated without stty rows/cols\n;; - Detached PTYs\n;;\n;; If you pass rows=0 to rendering code, (- rows 1) = -1, and\n;; (do ([r 0 (+ r 1)]) ((= r -1)) ...) loops forever.\n\n;; Fix 1: Guard at the protocol boundary (server-side resize handler)\n(define (handle-resize! client payload)\n (let ([raw-cols (bytevector-u16-ref payload 0 (endianness big))]\n [raw-rows (bytevector-u16-ref payload 2 (endianness big))])\n ;; Default 0x0 to 80x24\n (let ([cols (if (= raw-cols 0) 80 raw-cols)]\n [rows (if (= raw-rows 0) 24 raw-rows)])\n (client-cols-set! client cols)\n (client-rows-set! client rows))))\n\n;; Fix 2: Guard at the rendering boundary (defense in depth)\n(define (render-screen pane cols rows)\n (let ([cols (if (<= cols 0) 80 cols)]\n [rows (if (<= rows 0) 24 rows)])\n ;; Now (- rows 1) is always >= 0\n (let ([vt-rows (min (- rows 1) (vt-total-rows vt))])\n (do ([r 0 (+ r 1)])\n ((= r vt-rows))\n (render-row r)))))") ("id" . "jerboa-terminal-size-zero-guard") ("imports") ("notes" . "Root cause of mux server infinite loop after C-b c (new window). expect(1) creates PTYs with 0x0 window size by default (`spawn stty size` → '0 0'). The client faithfully sent 0x0 to the server, which stored it. When redraw-client! called render-single-pane with rows=0, the do-loop condition (= r -1) was unreachable. Always validate terminal dimensions at BOTH the protocol boundary AND the rendering boundary.") @@ -545,7 +547,7 @@ "Guard against 0x0 terminal size from TIOCGWINSZ")) (("code" . - ";; DANGER: do-loop with = test and negative bound loops forever!\n;;\n;; (do ([r 0 (+ r 1)]) ((= r -1)) body)\n;; r goes 0, 1, 2, 3, ... and NEVER equals -1.\n;;\n;; This happens when the termination value comes from arithmetic\n;; on unchecked input:\n;; (let ([rows 0])\n;; (do ([r 0 (+ r 1)])\n;; ((= r (- rows 1))) ; (= r -1) → infinite loop!\n;; (process-row r)))\n;;\n;; Fix: use >= instead of = for the termination test:\n(let ([rows 0])\n (do ([r 0 (+ r 1)])\n ((>= r (max 0 (- rows 1)))) ; safe: terminates immediately\n (process-row r)))\n\n;; Or guard the input:\n(let ([rows (max 1 rows)])\n (do ([r 0 (+ r 1)])\n ((= r (- rows 1)))\n (process-row r)))") ("id" . "chez-do-loop-negative-bound") ("imports") + ";; DANGER: do-loop with = test and negative bound loops forever!\n;;\n;; (do ([r 0 (+ r 1)]) ((= r -1)) body)\n;; r goes 0, 1, 2, 3, ... and NEVER equals -1.\n;;\n;; This happens when the termination value comes from arithmetic\n;; on unchecked input:\n;; (let ([rows 0])\n;; (do ([r 0 (+ r 1)])\n;; ((= r (- rows 1))) ; (= r -1) → infinite loop!\n;; (process-row r)))\n;;\n;; Fix: use >= instead of = for the termination test:\n(let ([rows 0])\n (do ([r 0 (+ r 1)])\n ((>= r (max 0 (- rows 1)))) ; safe: terminates immediately\n (process-row r)))\n\n;; Or guard the input:\n(let ([rows (max 1 rows)])\n (do ([r 0 (+ r 1)])\n ((= r (- rows 1)))\n (process-row r)))") ("id" . "jerboa-do-loop-negative-bound") ("imports") ("notes" . "Chez Scheme's do loop tests with = by convention. Unlike C for-loops which use <, the = test means the counter must hit the exact target value. With incrementing counters, a negative target is unreachable. This caused an infinite loop in the mux screen renderer when terminal rows was 0.") @@ -556,7 +558,7 @@ "Chez do-loop infinite loop with negative termination value")) (("code" . - ";; Chez SMP GC uses active_threads count (NOT S_nthreads) for rendezvous.\n;;\n;; Key C macros in types.h:\n;; deactivate_thread(tc) — sets ACTIVE(tc)=0, decrements active_threads.\n;; When active_threads reaches 0 and collect_request_pending, signals GC.\n;; reactivate_thread(tc) — sets ACTIVE(tc)=1, increments active_threads.\n;;\n;; Chez automatically deactivates threads during:\n;; - sleep (make-time based)\n;; - condition-wait (mutex + condition variable)\n;; - mutex-acquire (when blocked)\n;;\n;; Chez does NOT deactivate during foreign calls (c-lambda, foreign-procedure).\n;; If a thread blocks in a foreign call, it stays ACTIVE and GC waits for it.\n;;\n;; Public C API (in scheme.h):\n;; EXPORT int Sactivate_thread(void);\n;; EXPORT void Sdeactivate_thread(void);\n;;\n;; For Scheme-level code, use condition-wait or sleep around blocking\n;; operations. For C shims doing blocking I/O, call Sdeactivate_thread()\n;; before the blocking call and Sactivate_thread() after.\n;;\n;; CRITICAL: If your event loop calls blocking FFI on the primordial thread,\n;; it freezes the entire app — use background threads for blocking work\n;; and post results back via a UI queue.") ("id" . "chez-smp-gc-active-threads") ("imports") + ";; Chez SMP GC uses active_threads count (NOT S_nthreads) for rendezvous.\n;;\n;; Key C macros in types.h:\n;; deactivate_thread(tc) — sets ACTIVE(tc)=0, decrements active_threads.\n;; When active_threads reaches 0 and collect_request_pending, signals GC.\n;; reactivate_thread(tc) — sets ACTIVE(tc)=1, increments active_threads.\n;;\n;; Chez automatically deactivates threads during:\n;; - sleep (make-time based)\n;; - condition-wait (mutex + condition variable)\n;; - mutex-acquire (when blocked)\n;;\n;; Chez does NOT deactivate during foreign calls (c-lambda, foreign-procedure).\n;; If a thread blocks in a foreign call, it stays ACTIVE and GC waits for it.\n;;\n;; Public C API (in scheme.h):\n;; EXPORT int Sactivate_thread(void);\n;; EXPORT void Sdeactivate_thread(void);\n;;\n;; For Scheme-level code, use condition-wait or sleep around blocking\n;; operations. For C shims doing blocking I/O, call Sdeactivate_thread()\n;; before the blocking call and Sactivate_thread() after.\n;;\n;; CRITICAL: If your event loop calls blocking FFI on the primordial thread,\n;; it freezes the entire app — use background threads for blocking work\n;; and post results back via a UI queue.") ("id" . "jerboa-smp-gc-active-threads") ("imports") ("notes" . "Verified from ChezScheme/c/types.h lines 371-401. The GC rendezvous waits for active_threads==0, not for all S_nthreads to respond. Threads that call sleep/condition-wait/mutex-acquire are automatically deactivated. The practical fix for Qt apps: run blocking subprocess/file I/O in background threads (fork-thread cleans up properly), and only do Qt widget calls on the primordial thread via a UI action queue.") @@ -567,7 +569,7 @@ "Chez SMP GC: deactivate_thread for safe foreign calls")) (("code" . - "/* SSH chacha20-poly1305@openssh.com uses raw Poly1305 MAC,\n * NOT OpenSSL's EVP_chacha20_poly1305() which implements RFC 8439 AEAD.\n *\n * RFC 8439 AEAD pads AAD and ciphertext to 16-byte boundaries and\n * appends 8-byte lengths before computing the Poly1305 tag.\n * OpenSSH just runs raw Poly1305 over (enc_length || enc_payload).\n *\n * Correct implementation:\n * 1. Encrypt length (4 bytes) with K1/ChaCha20, counter=0\n * 2. Generate Poly1305 key: ChaCha20(K2, counter=0) → first 32 bytes\n * 3. Encrypt payload with K2/ChaCha20, counter=1\n * 4. MAC = raw Poly1305(enc_length || enc_payload, poly_key)\n *\n * Use EVP_chacha20() (plain) + EVP_MAC(\"POLY1305\") separately.\n * Do NOT use EVP_chacha20_poly1305() for SSH packets.\n */\n\n/* OpenSSL ChaCha20 IV format: counter(4 LE) || nonce(12) */\nstatic void build_chacha_iv(uint64_t seqno, uint32_t counter, uint8_t *iv16) {\n iv16[0] = counter & 0xFF;\n iv16[1] = (counter >> 8) & 0xFF;\n iv16[2] = (counter >> 16) & 0xFF;\n iv16[3] = (counter >> 24) & 0xFF;\n iv16[4] = 0; iv16[5] = 0; iv16[6] = 0; iv16[7] = 0;\n iv16[8] = (seqno >> 56) & 0xFF;\n iv16[9] = (seqno >> 48) & 0xFF;\n iv16[10] = (seqno >> 40) & 0xFF;\n iv16[11] = (seqno >> 32) & 0xFF;\n iv16[12] = (seqno >> 24) & 0xFF;\n iv16[13] = (seqno >> 16) & 0xFF;\n iv16[14] = (seqno >> 8) & 0xFF;\n iv16[15] = seqno & 0xFF;\n}\n\n/* Raw Poly1305 MAC via OpenSSL EVP_MAC */\nstatic int poly1305_mac(const uint8_t *key32,\n const uint8_t *data, int datalen, uint8_t *tag16) {\n EVP_MAC *mac = EVP_MAC_fetch(NULL, \"POLY1305\", NULL);\n EVP_MAC_CTX *mctx = EVP_MAC_CTX_new(mac);\n EVP_MAC_free(mac);\n size_t taglen = 16;\n EVP_MAC_init(mctx, key32, 32, NULL);\n EVP_MAC_update(mctx, data, datalen);\n EVP_MAC_final(mctx, tag16, &taglen, 16);\n EVP_MAC_CTX_free(mctx);\n return 0;\n}") ("id" . "chez-ssh-chacha20-poly1305-openssh") ("imports") + "/* SSH chacha20-poly1305@openssh.com uses raw Poly1305 MAC,\n * NOT OpenSSL's EVP_chacha20_poly1305() which implements RFC 8439 AEAD.\n *\n * RFC 8439 AEAD pads AAD and ciphertext to 16-byte boundaries and\n * appends 8-byte lengths before computing the Poly1305 tag.\n * OpenSSH just runs raw Poly1305 over (enc_length || enc_payload).\n *\n * Correct implementation:\n * 1. Encrypt length (4 bytes) with K1/ChaCha20, counter=0\n * 2. Generate Poly1305 key: ChaCha20(K2, counter=0) → first 32 bytes\n * 3. Encrypt payload with K2/ChaCha20, counter=1\n * 4. MAC = raw Poly1305(enc_length || enc_payload, poly_key)\n *\n * Use EVP_chacha20() (plain) + EVP_MAC(\"POLY1305\") separately.\n * Do NOT use EVP_chacha20_poly1305() for SSH packets.\n */\n\n/* OpenSSL ChaCha20 IV format: counter(4 LE) || nonce(12) */\nstatic void build_chacha_iv(uint64_t seqno, uint32_t counter, uint8_t *iv16) {\n iv16[0] = counter & 0xFF;\n iv16[1] = (counter >> 8) & 0xFF;\n iv16[2] = (counter >> 16) & 0xFF;\n iv16[3] = (counter >> 24) & 0xFF;\n iv16[4] = 0; iv16[5] = 0; iv16[6] = 0; iv16[7] = 0;\n iv16[8] = (seqno >> 56) & 0xFF;\n iv16[9] = (seqno >> 48) & 0xFF;\n iv16[10] = (seqno >> 40) & 0xFF;\n iv16[11] = (seqno >> 32) & 0xFF;\n iv16[12] = (seqno >> 24) & 0xFF;\n iv16[13] = (seqno >> 16) & 0xFF;\n iv16[14] = (seqno >> 8) & 0xFF;\n iv16[15] = seqno & 0xFF;\n}\n\n/* Raw Poly1305 MAC via OpenSSL EVP_MAC */\nstatic int poly1305_mac(const uint8_t *key32,\n const uint8_t *data, int datalen, uint8_t *tag16) {\n EVP_MAC *mac = EVP_MAC_fetch(NULL, \"POLY1305\", NULL);\n EVP_MAC_CTX *mctx = EVP_MAC_CTX_new(mac);\n EVP_MAC_free(mac);\n size_t taglen = 16;\n EVP_MAC_init(mctx, key32, 32, NULL);\n EVP_MAC_update(mctx, data, datalen);\n EVP_MAC_final(mctx, tag16, &taglen, 16);\n EVP_MAC_CTX_free(mctx);\n return 0;\n}") ("id" . "jerboa-ssh-chacha20-poly1305-openssh") ("imports") ("notes" . "CRITICAL: OpenSSL's EVP_chacha20_poly1305() implements IETF RFC 8439 AEAD construction which pads AAD/ciphertext to 16-byte boundaries and appends 8-byte LE lengths before Poly1305. OpenSSH's chacha20-poly1305@openssh.com runs raw Poly1305 over the plain concatenation. Using the wrong one produces valid-looking ciphertext but the server rejects it with 'message authentication code incorrect'. Symptom: kex succeeds, first encrypted packet fails. Key layout: key64 = K2(main, 32 bytes) || K1(length, 32 bytes).") @@ -578,7 +580,8 @@ "SSH ChaCha20-Poly1305: OpenSSH uses raw Poly1305, not RFC 8439 AEAD")) (("code" . - "(import (chezscheme))\n\n(define-record-type my-thing\n (fields\n (mutable active?) ;; field name includes the ?\n (mutable closed?)\n name)) ;; immutable field\n\n;; Accessor: my-thing-active? ← includes ?\n;; Mutator: my-thing-active?-set! ← ? comes BEFORE -set!\n;;\n;; WRONG: my-thing-active-set! ← missing the ?\n;; RIGHT: my-thing-active?-set!\n\n(define t (make-my-thing #f #f \"test\"))\n(my-thing-active? t) ;; => #f\n(my-thing-active?-set! t #t) ;; correct mutator\n(my-thing-active? t) ;; => #t\n\n;; Similarly:\n;; my-thing-closed? ← accessor\n;; my-thing-closed?-set! ← mutator (NOT my-thing-closed-set!)") ("id" . "chez-r6rs-record-question-mark-field") ("imports") + "(import (chezscheme))\n\n(define-record-type my-thing\n (fields\n (mutable active?) ;; field name includes the ?\n (mutable closed?)\n name)) ;; immutable field\n\n;; Accessor: my-thing-active? ← includes ?\n;; Mutator: my-thing-active?-set! ← ? comes BEFORE -set!\n;;\n;; WRONG: my-thing-active-set! ← missing the ?\n;; RIGHT: my-thing-active?-set!\n\n(define t (make-my-thing #f #f \"test\"))\n(my-thing-active? t) ;; => #f\n(my-thing-active?-set! t #t) ;; correct mutator\n(my-thing-active? t) ;; => #t\n\n;; Similarly:\n;; my-thing-closed? ← accessor\n;; my-thing-closed?-set! ← mutator (NOT my-thing-closed-set!)") ("id" . "jerboa-r6rs-record-question-mark-field") + ("imports") ("notes" . "The R6RS define-record-type naming convention appends -set! to the FULL field name. For fields named with ? (like eof?, closed?, active?), the mutator is recordtype-field?-set! — the ? is part of the field name and stays before -set!. Chez Scheme gives 'unbound identifier' at compile time if you use the wrong name. This is easy to miss because the natural English reading drops the ? before -set!.") @@ -589,7 +592,7 @@ "R6RS record fields named with ? suffix: mutator is name?-set!")) (("code" . - "(import (chezscheme))\n\n;; WRONG: Using 0 as sentinel for an optional integer field\n;; Problem: 0 is a valid value (e.g., server can assign channel-id 0)\n(define-record-type bad-channel\n (fields (mutable remote-id)) ;; initialized to 0\n (protocol (lambda (new) (lambda () (new 0)))))\n\n;; Then checking (> (bad-channel-remote-id ch) 0) never becomes true\n;; when server assigns id 0 → infinite loop!\n\n;; RIGHT: Use #f as sentinel, check with (channel-remote-id ch)\n(define-record-type good-channel\n (fields (mutable remote-id)) ;; initialized to #f\n (protocol (lambda (new) (lambda () (new #f)))))\n\n;; Check: (if (good-channel-remote-id ch) ...has-value... ...no-value...)\n;; Works for any integer including 0") ("id" . "chez-r6rs-record-sentinel-for-optional-int") + "(import (chezscheme))\n\n;; WRONG: Using 0 as sentinel for an optional integer field\n;; Problem: 0 is a valid value (e.g., server can assign channel-id 0)\n(define-record-type bad-channel\n (fields (mutable remote-id)) ;; initialized to 0\n (protocol (lambda (new) (lambda () (new 0)))))\n\n;; Then checking (> (bad-channel-remote-id ch) 0) never becomes true\n;; when server assigns id 0 → infinite loop!\n\n;; RIGHT: Use #f as sentinel, check with (channel-remote-id ch)\n(define-record-type good-channel\n (fields (mutable remote-id)) ;; initialized to #f\n (protocol (lambda (new) (lambda () (new #f)))))\n\n;; Check: (if (good-channel-remote-id ch) ...has-value... ...no-value...)\n;; Works for any integer including 0") ("id" . "jerboa-r6rs-record-sentinel-for-optional-int") ("imports") ("notes" . @@ -601,7 +604,7 @@ "Use #f not 0 as sentinel for optional integer record fields")) (("code" . - "(import (chezscheme))\n\n;; WRONG: Record generates channel-table-next-id accessor,\n;; then you define a function with the same name\n(define-record-type channel-table\n (fields (mutable next-id))) ;; generates channel-table-next-id\n\n;; This CONFLICTS:\n;; (define (channel-table-next-id table) ...)\n;; Error: multiple definitions for channel-table-next-id\n\n;; RIGHT: Use a different name for the custom function\n(define (channel-table-alloc-id table)\n (let ([id (channel-table-next-id table)]) ;; use the record accessor\n (channel-table-next-id-set! table (+ id 1))\n id))") ("id" . "chez-r6rs-library-duplicate-definition") + "(import (chezscheme))\n\n;; WRONG: Record generates channel-table-next-id accessor,\n;; then you define a function with the same name\n(define-record-type channel-table\n (fields (mutable next-id))) ;; generates channel-table-next-id\n\n;; This CONFLICTS:\n;; (define (channel-table-next-id table) ...)\n;; Error: multiple definitions for channel-table-next-id\n\n;; RIGHT: Use a different name for the custom function\n(define (channel-table-alloc-id table)\n (let ([id (channel-table-next-id table)]) ;; use the record accessor\n (channel-table-next-id-set! table (+ id 1))\n id))") ("id" . "jerboa-r6rs-library-duplicate-definition") ("imports") ("notes" . @@ -613,7 +616,7 @@ "R6RS library duplicate definition: record accessor vs custom function")) (("code" . - ";; Two paths detect dead panes in a mux server event loop:\n;;\n;; 1. SIGCHLD handler → waitpid(-1, WNOHANG) loop → mark pane alive?=#f\n;; 2. PTY master read returns EIO/EOF → mark pane alive?=#f\n;;\n;; Then cleanup removes dead panes from windows and empty windows from sessions:\n\n(define (sm-cleanup-dead-panes! sm)\n (let ([changed? #f])\n (for-each\n (lambda (session)\n (for-each\n (lambda (win)\n ;; Close master fds on dead panes\n (for-each\n (lambda (pane)\n (unless (mux-pane-alive? pane)\n (guard (e [#t (void)])\n (ffi-stream-close (mux-pane-master-fd pane)))))\n (mux-window-panes win))\n ;; Remove dead panes\n (let ([live (filter mux-pane-alive? (mux-window-panes win))])\n (when (< (length live) (length (mux-window-panes win)))\n (set! changed? #t)\n (mux-window-panes-set! win live)\n ;; Clamp active pane index\n (when (and (pair? live)\n (>= (mux-window-active-pane-idx win) (length live)))\n (mux-window-active-pane-idx-set! win (- (length live) 1))))))\n (mux-session-windows session))\n ;; Remove empty windows\n (let ([live-wins (filter (lambda (w) (pair? (mux-window-panes w)))\n (mux-session-windows session))])\n (when (< (length live-wins) (length (mux-session-windows session)))\n (set! changed? #t)\n (mux-session-windows-set! session live-wins)\n (when (and (pair? live-wins)\n (>= (mux-session-active-window-idx session) (length live-wins)))\n (mux-session-active-window-idx-set! session (- (length live-wins) 1))))))\n (sm-sessions sm))\n changed?))\n\n;; In the event loop, call after handle-pty-output!:\n;; (when (sm-cleanup-dead-panes! sm)\n;; (redraw-all-clients! state)\n;; (when (all-sessions-empty? sm)\n;; (shutdown-server! state)))") ("id" . "chez-mux-dead-pane-cleanup") + ";; Two paths detect dead panes in a mux server event loop:\n;;\n;; 1. SIGCHLD handler → waitpid(-1, WNOHANG) loop → mark pane alive?=#f\n;; 2. PTY master read returns EIO/EOF → mark pane alive?=#f\n;;\n;; Then cleanup removes dead panes from windows and empty windows from sessions:\n\n(define (sm-cleanup-dead-panes! sm)\n (let ([changed? #f])\n (for-each\n (lambda (session)\n (for-each\n (lambda (win)\n ;; Close master fds on dead panes\n (for-each\n (lambda (pane)\n (unless (mux-pane-alive? pane)\n (guard (e [#t (void)])\n (ffi-stream-close (mux-pane-master-fd pane)))))\n (mux-window-panes win))\n ;; Remove dead panes\n (let ([live (filter mux-pane-alive? (mux-window-panes win))])\n (when (< (length live) (length (mux-window-panes win)))\n (set! changed? #t)\n (mux-window-panes-set! win live)\n ;; Clamp active pane index\n (when (and (pair? live)\n (>= (mux-window-active-pane-idx win) (length live)))\n (mux-window-active-pane-idx-set! win (- (length live) 1))))))\n (mux-session-windows session))\n ;; Remove empty windows\n (let ([live-wins (filter (lambda (w) (pair? (mux-window-panes w)))\n (mux-session-windows session))])\n (when (< (length live-wins) (length (mux-session-windows session)))\n (set! changed? #t)\n (mux-session-windows-set! session live-wins)\n (when (and (pair? live-wins)\n (>= (mux-session-active-window-idx session) (length live-wins)))\n (mux-session-active-window-idx-set! session (- (length live-wins) 1))))))\n (sm-sessions sm))\n changed?))\n\n;; In the event loop, call after handle-pty-output!:\n;; (when (sm-cleanup-dead-panes! sm)\n;; (redraw-all-clients! state)\n;; (when (all-sessions-empty? sm)\n;; (shutdown-server! state)))") ("id" . "jerboa-mux-dead-pane-cleanup") ("imports" "(chezscheme)") ("notes" . @@ -625,7 +628,7 @@ "Mux server: reap dead panes and remove empty windows")) (("code" . - ";; In C, after openpty() + fork():\nint ffi_pty_fork_exec(...) {\n int master, slave;\n openpty(&master, &slave, NULL, NULL, NULL);\n pid_t child = fork();\n if (child > 0) {\n close(slave);\n /* CRITICAL: Set non-blocking so event loop doesn't stall */\n fcntl(master, F_SETFL, fcntl(master, F_GETFL) | O_NONBLOCK);\n return child;\n }\n /* child: setsid, TIOCSCTTY, dup2 slave→0/1/2, exec */\n}\n\n;; In Scheme, the non-blocking read wrapper:\n;; Returns: >0 (bytes read), 0 (EAGAIN, nothing available), -1 (EOF/error)\nint ffi_bv_read_nonblock(int fd, unsigned char *buf, int offset, int max_len) {\n ssize_t n;\n do { n = read(fd, buf + offset, max_len); } while (n < 0 && errno == EINTR);\n if (n < 0) {\n if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;\n return -1; /* real error (EIO when slave closes) */\n }\n if (n == 0) return -1; /* EOF */\n return (int)n;\n}\n\n;; Scheme event loop reads PTY output:\n;; n > 0 → got data, send to clients\n;; n = 0 → EAGAIN, nothing available (normal)\n;; n < 0 → EOF/EIO, mark pane dead") ("id" . "chez-pty-nonblock-eof") ("imports") + ";; In C, after openpty() + fork():\nint ffi_pty_fork_exec(...) {\n int master, slave;\n openpty(&master, &slave, NULL, NULL, NULL);\n pid_t child = fork();\n if (child > 0) {\n close(slave);\n /* CRITICAL: Set non-blocking so event loop doesn't stall */\n fcntl(master, F_SETFL, fcntl(master, F_GETFL) | O_NONBLOCK);\n return child;\n }\n /* child: setsid, TIOCSCTTY, dup2 slave→0/1/2, exec */\n}\n\n;; In Scheme, the non-blocking read wrapper:\n;; Returns: >0 (bytes read), 0 (EAGAIN, nothing available), -1 (EOF/error)\nint ffi_bv_read_nonblock(int fd, unsigned char *buf, int offset, int max_len) {\n ssize_t n;\n do { n = read(fd, buf + offset, max_len); } while (n < 0 && errno == EINTR);\n if (n < 0) {\n if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;\n return -1; /* real error (EIO when slave closes) */\n }\n if (n == 0) return -1; /* EOF */\n return (int)n;\n}\n\n;; Scheme event loop reads PTY output:\n;; n > 0 → got data, send to clients\n;; n = 0 → EAGAIN, nothing available (normal)\n;; n < 0 → EOF/EIO, mark pane dead") ("id" . "jerboa-pty-nonblock-eof") ("imports") ("notes" . "When a PTY slave is closed (shell exits), the master fd returns EIO on read — not EOF (0). The C wrapper maps both to -1 for simplicity. Without O_NONBLOCK, the read() would block the entire single-threaded event loop until data arrives. EINTR retry is essential because SIGCHLD interrupts read().") @@ -635,7 +638,7 @@ "PTY master fd must be O_NONBLOCK for event loop polling")) (("code" . - ";; Wire format: Type(1 byte) + Length(4 bytes big-endian) + Payload(variable)\n;;\n;; Message types:\n;; MSG-ATTACH=#x01 MSG-DETACH=#x02 MSG-INPUT=#x03 MSG-RESIZE=#x04\n;; MSG-OUTPUT=#x05 MSG-REDRAW=#x06 MSG-CMD=#x07 MSG-STATUS=#x08\n;;\n;; Encode:\n(define (mux-encode type payload)\n (let* ([data (if (bytevector? payload) payload (string->utf8 payload))]\n [len (bytevector-length data)]\n [msg (make-bytevector (+ 5 len))])\n (bytevector-u8-set! msg 0 type)\n (bytevector-u8-set! msg 1 (bitwise-and (ash len -24) #xff))\n (bytevector-u8-set! msg 2 (bitwise-and (ash len -16) #xff))\n (bytevector-u8-set! msg 3 (bitwise-and (ash len -8) #xff))\n (bytevector-u8-set! msg 4 (bitwise-and len #xff))\n (bytevector-copy! data 0 msg 5 len)\n msg))\n\n;; Decode:\n(define (mux-decode msg)\n (if (< (bytevector-length msg) 5)\n (values #f #f)\n (let* ([type (bytevector-u8-ref msg 0)]\n [len (bitwise-ior\n (ash (bytevector-u8-ref msg 1) 24)\n (ash (bytevector-u8-ref msg 2) 16)\n (ash (bytevector-u8-ref msg 3) 8)\n (bytevector-u8-ref msg 4))])\n (values type (subbytevector msg 5 (+ 5 len))))))") ("id" . "chez-mux-wire-protocol") + ";; Wire format: Type(1 byte) + Length(4 bytes big-endian) + Payload(variable)\n;;\n;; Message types:\n;; MSG-ATTACH=#x01 MSG-DETACH=#x02 MSG-INPUT=#x03 MSG-RESIZE=#x04\n;; MSG-OUTPUT=#x05 MSG-REDRAW=#x06 MSG-CMD=#x07 MSG-STATUS=#x08\n;;\n;; Encode:\n(define (mux-encode type payload)\n (let* ([data (if (bytevector? payload) payload (string->utf8 payload))]\n [len (bytevector-length data)]\n [msg (make-bytevector (+ 5 len))])\n (bytevector-u8-set! msg 0 type)\n (bytevector-u8-set! msg 1 (bitwise-and (ash len -24) #xff))\n (bytevector-u8-set! msg 2 (bitwise-and (ash len -16) #xff))\n (bytevector-u8-set! msg 3 (bitwise-and (ash len -8) #xff))\n (bytevector-u8-set! msg 4 (bitwise-and len #xff))\n (bytevector-copy! data 0 msg 5 len)\n msg))\n\n;; Decode:\n(define (mux-decode msg)\n (if (< (bytevector-length msg) 5)\n (values #f #f)\n (let* ([type (bytevector-u8-ref msg 0)]\n [len (bitwise-ior\n (ash (bytevector-u8-ref msg 1) 24)\n (ash (bytevector-u8-ref msg 2) 16)\n (ash (bytevector-u8-ref msg 3) 8)\n (bytevector-u8-ref msg 4))])\n (values type (subbytevector msg 5 (+ 5 len))))))") ("id" . "jerboa-mux-wire-protocol") ("imports" "(chezscheme)") ("notes" . @@ -647,19 +650,19 @@ "Mux wire protocol: [Type:1B][Len:4B BE][Payload]")) (("code" . - ";; Load chez-ssh at runtime (not compile time) to avoid hard OpenSSL dependency\n(define ssh-available?\n (let ([loaded? #f] [available? #f])\n (lambda ()\n (unless loaded?\n (set! loaded? #t)\n (guard (e [#t (set! available? #f)])\n (eval '(import (chez-ssh)) (interaction-environment))\n (set! available? #t)))\n available?)))\n\n;; Call chez-ssh functions through eval:\n(define (ssh-call proc-name . args)\n (let ([proc (eval proc-name (interaction-environment))])\n (apply proc args)))\n\n;; Usage:\n(when (ssh-available?)\n (let ([conn (ssh-call 'ssh-connect \"host\" 22 \"user\")])\n (let ([result (ssh-call 'ssh-run conn \"ls -la\")])\n (display (cdr result)))\n (ssh-call 'ssh-disconnect conn)))") ("id" . "chez-ssh-dynamic-import") + ";; Load jerboa-ssh at runtime (not compile time) to avoid hard OpenSSL dependency\n(define ssh-available?\n (let ([loaded? #f] [available? #f])\n (lambda ()\n (unless loaded?\n (set! loaded? #t)\n (guard (e [#t (set! available? #f)])\n (eval '(import (jerboa-ssh)) (interaction-environment))\n (set! available? #t)))\n available?)))\n\n;; Call jerboa-ssh functions through eval:\n(define (ssh-call proc-name . args)\n (let ([proc (eval proc-name (interaction-environment))])\n (apply proc args)))\n\n;; Usage:\n(when (ssh-available?)\n (let ([conn (ssh-call 'ssh-connect \"host\" 22 \"user\")])\n (let ([result (ssh-call 'ssh-run conn \"ls -la\")])\n (display (cdr result)))\n (ssh-call 'ssh-disconnect conn)))") ("id" . "jerboa-ssh-dynamic-import") ("imports" "(chezscheme)") ("notes" . - "The eval + interaction-environment pattern avoids compile-time dependency on (chez-ssh). If OpenSSL is not linked or chez-ssh is not installed, the guard catches the import error and ssh-available? returns #f. All ssh function calls go through ssh-call which evals the proc name at runtime. This is necessary for static musl binaries where OpenSSL may or may not be linked.") + "The eval + interaction-environment pattern avoids compile-time dependency on (jerboa-ssh). If OpenSSL is not linked or jerboa-ssh is not installed, the guard catches the import error and ssh-available? returns #f. All ssh function calls go through ssh-call which evals the proc name at runtime. This is necessary for static musl binaries where OpenSSL may or may not be linked.") ("tags" "ssh" "dynamic-import" "eval" "interaction-environment" "optional-dependency") ("title" . - "Dynamically import chez-ssh to avoid hard dependency")) + "Dynamically import jerboa-ssh to avoid hard dependency")) (("code" . - ";; Interactive SSH session with PTY:\n;;\n;; 1. Open channel, request PTY with real terminal size\n;; 2. Put local terminal in raw mode\n;; 3. Relay loop: stdin→SSH, SSH→stdout\n;; 4. Restore terminal on exit\n\n(ffi-termios-save 0 1) ;; save terminal state\n(ffi-set-raw-mode 0) ;; raw mode for stdin\n(ffi-signal-flag-install 28) ;; SIGWINCH\n\n(guard (e [#t (ffi-termios-restore 0 1)]) ;; always restore\n (let loop ()\n (let ([eof? (ssh-call 'ssh-channel-eof? ch)]\n [closed? (ssh-call 'ssh-channel-closed? ch)])\n (cond\n [(or eof? closed?) (or (ssh-call 'ssh-channel-exit-status ch) 0)]\n [else\n ;; stdin → SSH channel (byte at a time)\n (when (ffi-byte-ready? 0)\n (let ([byte (ffi-read-byte 0)])\n (when (>= byte 0)\n (ssh-call 'ssh-channel-send-data ts ch\n (make-bytevector 1 byte)))))\n ;; SSH → stdout (dispatch + drain queues)\n (when (ffi-byte-ready? ssh-fd)\n (ssh-call 'ssh-channel-dispatch ts table)\n ;; Drain data queue\n (let drain ()\n (let ([q (ssh-call 'ssh-channel-data-queue ch)])\n (when (pair? q)\n (ffi-fdwrite 1 (car q))\n (ssh-call 'ssh-channel-data-queue-set! ch (cdr q))\n (drain)))))\n ;; Handle SIGWINCH\n (when (= (ffi-signal-flag-check 28) 1)\n (send-window-change-request ts ch\n (ffi-terminal-columns 0) (ffi-terminal-rows 0)))\n (ffi-nanosleep-us 500)\n (loop)]))))\n(ffi-termios-restore 0 1) ;; restore terminal") ("id" . "chez-ssh-pty-relay") ("imports" "(chezscheme)") + ";; Interactive SSH session with PTY:\n;;\n;; 1. Open channel, request PTY with real terminal size\n;; 2. Put local terminal in raw mode\n;; 3. Relay loop: stdin→SSH, SSH→stdout\n;; 4. Restore terminal on exit\n\n(ffi-termios-save 0 1) ;; save terminal state\n(ffi-set-raw-mode 0) ;; raw mode for stdin\n(ffi-signal-flag-install 28) ;; SIGWINCH\n\n(guard (e [#t (ffi-termios-restore 0 1)]) ;; always restore\n (let loop ()\n (let ([eof? (ssh-call 'ssh-channel-eof? ch)]\n [closed? (ssh-call 'ssh-channel-closed? ch)])\n (cond\n [(or eof? closed?) (or (ssh-call 'ssh-channel-exit-status ch) 0)]\n [else\n ;; stdin → SSH channel (byte at a time)\n (when (ffi-byte-ready? 0)\n (let ([byte (ffi-read-byte 0)])\n (when (>= byte 0)\n (ssh-call 'ssh-channel-send-data ts ch\n (make-bytevector 1 byte)))))\n ;; SSH → stdout (dispatch + drain queues)\n (when (ffi-byte-ready? ssh-fd)\n (ssh-call 'ssh-channel-dispatch ts table)\n ;; Drain data queue\n (let drain ()\n (let ([q (ssh-call 'ssh-channel-data-queue ch)])\n (when (pair? q)\n (ffi-fdwrite 1 (car q))\n (ssh-call 'ssh-channel-data-queue-set! ch (cdr q))\n (drain)))))\n ;; Handle SIGWINCH\n (when (= (ffi-signal-flag-check 28) 1)\n (send-window-change-request ts ch\n (ffi-terminal-columns 0) (ffi-terminal-rows 0)))\n (ffi-nanosleep-us 500)\n (loop)]))))\n(ffi-termios-restore 0 1) ;; restore terminal") ("id" . "jerboa-ssh-pty-relay") ("imports" "(chezscheme)") ("notes" . "Key patterns: (1) ffi-byte-ready? polls fd without blocking — essential for single-threaded relay. (2) Drain data/stderr queues directly instead of ssh-channel-read which blocks. (3) Send window-adjust packets after consuming data to keep flow control happy. (4) Always restore terminal in guard clause to avoid leaving terminal in raw mode on error. (5) SIGWINCH (signal 28) triggers window-change channel request to server.") @@ -670,7 +673,7 @@ "SSH interactive PTY relay loop with raw terminal")) (("code" . - ";; When a daemon is started via fork-exec using /proc/self/exe:\n(define jsh-path \"/proc/self/exe\")\n(ffi-fork-exec jsh-path argv \"\" -1 \"\" \"\")\n\n;; The forked process gets the CURRENT binary. But after rebuilding,\n;; /proc/<pid>/exe shows:\n;; /path/to/binary (deleted)\n;;\n;; The running daemon still executes the OLD code in memory.\n;; Rebuilding does NOT update it — you must:\n;; 1. Kill the old daemon\n;; 2. Start a new one with the rebuilt binary\n;;\n;; Check for stale daemons:\n;; $ ls -la /proc/<pid>/exe\n;; lrwxrwxrwx ... /path/to/jsh-musl (deleted) ← STALE!") ("id" . "chez-proc-self-exe-stale") ("imports") + ";; When a daemon is started via fork-exec using /proc/self/exe:\n(define jsh-path \"/proc/self/exe\")\n(ffi-fork-exec jsh-path argv \"\" -1 \"\" \"\")\n\n;; The forked process gets the CURRENT binary. But after rebuilding,\n;; /proc/<pid>/exe shows:\n;; /path/to/binary (deleted)\n;;\n;; The running daemon still executes the OLD code in memory.\n;; Rebuilding does NOT update it — you must:\n;; 1. Kill the old daemon\n;; 2. Start a new one with the rebuilt binary\n;;\n;; Check for stale daemons:\n;; $ ls -la /proc/<pid>/exe\n;; lrwxrwxrwx ... /path/to/jsh-musl (deleted) ← STALE!") ("id" . "jerboa-proc-self-exe-stale") ("imports") ("notes" . "A running process keeps the old binary code in memory even after the file is overwritten by a rebuild. /proc/<pid>/exe will show '(deleted)' when the on-disk binary no longer matches. This is the #1 cause of 'my fix doesn't work' after rebuilding — you forgot to restart the daemon. Always kill and restart daemons after rebuilding.") @@ -681,7 +684,7 @@ "/proc/self/exe and stale daemon processes after rebuild")) (("code" . - ";; After ,unlock succeeds, source //embed/.jshrc to apply PS1, aliases, etc.\n;; without requiring a shell restart:\n(let ([rc (guard (e [#t #f])\n (embed-file->string \"//embed/.jshrc\"))])\n (when rc (run-cmd rc)))\n\n;; embed-file->string reads from the binary's embedded file store\n;; The //embed/ prefix is a virtual path using POSIX §4.12 (// is implementation-defined)\n;; run-cmd executes the string as shell commands in the current environment") ("id" . "chez-embed-source-on-unlock") ("imports") + ";; After ,unlock succeeds, source //embed/.jshrc to apply PS1, aliases, etc.\n;; without requiring a shell restart:\n(let ([rc (guard (e [#t #f])\n (embed-file->string \"//embed/.jshrc\"))])\n (when rc (run-cmd rc)))\n\n;; embed-file->string reads from the binary's embedded file store\n;; The //embed/ prefix is a virtual path using POSIX §4.12 (// is implementation-defined)\n;; run-cmd executes the string as shell commands in the current environment") ("id" . "jerboa-embed-source-on-unlock") ("imports") ("notes" . "embed-file->string returns the UTF-8 content of an embedded file, or raises an error if not found. Guard against missing files. run-cmd parses and executes shell commands, so the .jshrc content is interpreted as shell script (not Scheme). This is used after ,unlock because embedded files are encrypted at rest and only available after decryption.") @@ -1149,7 +1152,7 @@ ("notes" . "R6RS specifies (bytevector-copy bv) with 1 argument. Chez extends it to (bytevector-copy bv start count) but prints a warning. Use this bv-sub helper to silence it. Common in FFI code where you receive data in a pre-allocated buffer and need to extract exactly len bytes.") - ("superseded_by" . "chez-round12-core-prims") + ("superseded_by" . "jerboa-round12-core-prims") ("tags" "bytevector" "r6rs" "copy" "sub-bytevector" "ffi" "warning") ("title" @@ -1356,7 +1359,7 @@ "Rust rusqlite from Chez Scheme with Handle-Based API")) (("code" . - ";; In static binaries, symbols are registered via Sforeign_symbol()\n;; at startup, so load-shared-object is unnecessary and may crash.\n;;\n;; Pattern: try loading but don't error if it fails\n(or (guard (e [#t #f])\n (load-shared-object \"libmylib.so\")\n #t)\n #t)\n\n;; This succeeds in BOTH cases:\n;; - Dynamic build: loads the .so normally\n;; - Static build: guard catches the error, falls through to #t\n;;\n;; The symbols are already available either way:\n;; - Dynamic: loaded from .so\n;; - Static: registered via Sforeign_symbol in main()") ("id" . "chez-load-shared-object-static") + ";; In static binaries, symbols are registered via Sforeign_symbol()\n;; at startup, so load-shared-object is unnecessary and may crash.\n;;\n;; Pattern: try loading but don't error if it fails\n(or (guard (e [#t #f])\n (load-shared-object \"libmylib.so\")\n #t)\n #t)\n\n;; This succeeds in BOTH cases:\n;; - Dynamic build: loads the .so normally\n;; - Static build: guard catches the error, falls through to #t\n;;\n;; The symbols are already available either way:\n;; - Dynamic: loaded from .so\n;; - Static: registered via Sforeign_symbol in main()") ("id" . "jerboa-load-shared-object-static") ("imports" "(chezscheme)") ("notes" . @@ -1474,7 +1477,7 @@ "Clojure-style get-in / assoc-in / update-in (nested access)")) (("code" . - ";; Guardian-based leak detection for handles that must be explicitly closed.\n;; When a handle is GC'd without close, the guardian fires and we warn + cleanup.\n\n(define *resource-guardian* (make-guardian))\n\n(define *resource-finalizer-log*\n (make-parameter\n (lambda (type info)\n (fprintf (current-error-port)\n \"WARNING: ~a handle GC'd without close! (~a)~%\" type info))))\n\n(define (register-guarded-resource! handle type info cleanup-proc)\n ;; entry: #(type info cleanup closed?)\n (let ([entry (vector type info cleanup-proc #f)])\n (*resource-guardian* entry)\n entry))\n\n(define (mark-resource-closed! entry)\n (when entry (vector-set! entry 3 #t)))\n\n(define (poll-resource-finalizers!)\n ;; Call periodically or at shutdown. Returns leaked count.\n (let loop ([count 0])\n (let ([entry (*resource-guardian*)])\n (if (not entry) count\n (begin\n (unless (vector-ref entry 3) ;; not closed?\n (let ([type (vector-ref entry 0)]\n [info (vector-ref entry 1)]\n [cleanup (vector-ref entry 2)])\n ((*resource-finalizer-log*) type info)\n (guard (exn [#t (void)])\n (when cleanup (cleanup)))))\n (loop (+ count 1)))))))") ("id" . "chez-guardian-resource-leak-detection") + ";; Guardian-based leak detection for handles that must be explicitly closed.\n;; When a handle is GC'd without close, the guardian fires and we warn + cleanup.\n\n(define *resource-guardian* (make-guardian))\n\n(define *resource-finalizer-log*\n (make-parameter\n (lambda (type info)\n (fprintf (current-error-port)\n \"WARNING: ~a handle GC'd without close! (~a)~%\" type info))))\n\n(define (register-guarded-resource! handle type info cleanup-proc)\n ;; entry: #(type info cleanup closed?)\n (let ([entry (vector type info cleanup-proc #f)])\n (*resource-guardian* entry)\n entry))\n\n(define (mark-resource-closed! entry)\n (when entry (vector-set! entry 3 #t)))\n\n(define (poll-resource-finalizers!)\n ;; Call periodically or at shutdown. Returns leaked count.\n (let loop ([count 0])\n (let ([entry (*resource-guardian*)])\n (if (not entry) count\n (begin\n (unless (vector-ref entry 3) ;; not closed?\n (let ([type (vector-ref entry 0)]\n [info (vector-ref entry 1)]\n [cleanup (vector-ref entry 2)])\n ((*resource-finalizer-log*) type info)\n (guard (exn [#t (void)])\n (when cleanup (cleanup)))))\n (loop (+ count 1)))))))") ("id" . "jerboa-guardian-resource-leak-detection") ("imports") ("notes" . @@ -1516,7 +1519,7 @@ "Runtime SQL injection heuristic detection in Scheme")) (("code" . - ";; Pack a C struct into foreign memory for passing to syscalls\\n;; Example: landlock_ruleset_attr { u64 handled_access_fs; } = 8 bytes\\n(let ([mem (foreign-alloc 8)])\\n (foreign-set! 'unsigned-64 mem 0 #xFFFF) ;; set u64 at offset 0\\n ;; Pass mem as 'long' arg to syscall (on x86_64, long = pointer size)\\n (let ([result (foreign-procedure \\\"syscall\\\" (long long long) long)\\n SYS_landlock_create_ruleset mem 8 0)])\\n (foreign-free mem))\\n\\n;; For multi-field structs, compute offsets manually:\\n;; struct { u64 allowed_access; s32 parent_fd; } = 12 bytes\\n(let ([mem (foreign-alloc 12)])\\n (foreign-set! 'unsigned-64 mem 0 access-bits) ;; offset 0: u64\\n (foreign-set! 'integer-32 mem 8 fd) ;; offset 8: s32\\n ;; ... use mem ...\\n (foreign-free mem))") ("id" . "chez-foreign-alloc-struct-packing") + ";; Pack a C struct into foreign memory for passing to syscalls\\n;; Example: landlock_ruleset_attr { u64 handled_access_fs; } = 8 bytes\\n(let ([mem (foreign-alloc 8)])\\n (foreign-set! 'unsigned-64 mem 0 #xFFFF) ;; set u64 at offset 0\\n ;; Pass mem as 'long' arg to syscall (on x86_64, long = pointer size)\\n (let ([result (foreign-procedure \\\"syscall\\\" (long long long) long)\\n SYS_landlock_create_ruleset mem 8 0)])\\n (foreign-free mem))\\n\\n;; For multi-field structs, compute offsets manually:\\n;; struct { u64 allowed_access; s32 parent_fd; } = 12 bytes\\n(let ([mem (foreign-alloc 12)])\\n (foreign-set! 'unsigned-64 mem 0 access-bits) ;; offset 0: u64\\n (foreign-set! 'integer-32 mem 8 fd) ;; offset 8: s32\\n ;; ... use mem ...\\n (foreign-free mem))") ("id" . "jerboa-foreign-alloc-struct-packing") ("imports" "(chezscheme)") ("notes" . @@ -1528,7 +1531,7 @@ "Pack C structs with foreign-alloc for syscalls (Chez Scheme)")) (("code" . - ";; Problem: A helper called from a syntax transformer runs at expand time (phase 1)\\n;; but plain 'define' is phase 0. This causes:\\n;; \\\"attempt to reference out-of-phase identifier\\\"\\n;;\\n;; Solution: Use 'meta define' for expand-time helpers\\n;; and import dependencies with (for ... run expand)\\n\\n(library (my-lib)\\n (export my-macro)\\n (import (chezscheme)\\n (for (some-dep) run expand)) ;; available at BOTH phases\\n\\n ;; This runs at expand time (phase 1) — visible to syntax transformers\\n (meta define (expand-time-helper arg)\\n (do-something-with arg))\\n\\n ;; This runs at run time (phase 0) — NOT visible to macros\\n (define (runtime-helper arg)\\n (do-something-else arg))\\n\\n (define-syntax my-macro\\n (lambda (stx)\\n ;; Can call expand-time-helper here\\n (expand-time-helper (syntax->datum stx))\\n ;; CANNOT call runtime-helper here — wrong phase!\\n #'(void))))") ("id" . "chez-meta-define-expand-phase") + ";; Problem: A helper called from a syntax transformer runs at expand time (phase 1)\\n;; but plain 'define' is phase 0. This causes:\\n;; \\\"attempt to reference out-of-phase identifier\\\"\\n;;\\n;; Solution: Use 'meta define' for expand-time helpers\\n;; and import dependencies with (for ... run expand)\\n\\n(library (my-lib)\\n (export my-macro)\\n (import (chezscheme)\\n (for (some-dep) run expand)) ;; available at BOTH phases\\n\\n ;; This runs at expand time (phase 1) — visible to syntax transformers\\n (meta define (expand-time-helper arg)\\n (do-something-with arg))\\n\\n ;; This runs at run time (phase 0) — NOT visible to macros\\n (define (runtime-helper arg)\\n (do-something-else arg))\\n\\n (define-syntax my-macro\\n (lambda (stx)\\n ;; Can call expand-time-helper here\\n (expand-time-helper (syntax->datum stx))\\n ;; CANNOT call runtime-helper here — wrong phase!\\n #'(void))))") ("id" . "jerboa-meta-define-expand-phase") ("imports" "(chezscheme)") ("notes" . @@ -1540,7 +1543,7 @@ "Use meta define for expand-time bindings in R6RS libraries")) (("code" . - ";; WRONG: Shell injection via user-controlled path\\n;; (system (string-append \\\"mkdir -p \\\" path)) ;; VULNERABLE!\\n;;\\n;; RIGHT: Pure Scheme recursive mkdir\\n(define (split-path path)\\n (let loop ([i 0] [start 0] [parts '()])\\n (cond\\n [(= i (string-length path))\\n (reverse (if (> i start)\\n (cons (substring path start i) parts)\\n parts))]\\n [(char=? (string-ref path i) #\\\\/)\\n (loop (+ i 1) (+ i 1)\\n (if (> i start)\\n (cons (substring path start i) parts)\\n parts))]\\n [else (loop (+ i 1) start parts)])))\\n\\n(define (mkdir-p path)\\n (when (string-contains-char path #\\\\nul)\\n (error 'mkdir-p \\\"path contains null byte\\\" path))\\n (let ([components (split-path path)])\\n (let loop ([parts components] [current \\\"\\\"])\\n (unless (null? parts)\\n (let ([dir (if (string=? current \\\"\\\")\\n (car parts)\\n (string-append current \\\"/\\\" (car parts)))])\\n (unless (or (string=? dir \\\"\\\") (file-directory? dir))\\n (guard (exn [#t (void)]) ;; ignore EEXIST races\\n (mkdir dir #o755)))\\n (loop (cdr parts) dir))))))") ("id" . "chez-safe-mkdir-p") ("imports" "(chezscheme)") + ";; WRONG: Shell injection via user-controlled path\\n;; (system (string-append \\\"mkdir -p \\\" path)) ;; VULNERABLE!\\n;;\\n;; RIGHT: Pure Scheme recursive mkdir\\n(define (split-path path)\\n (let loop ([i 0] [start 0] [parts '()])\\n (cond\\n [(= i (string-length path))\\n (reverse (if (> i start)\\n (cons (substring path start i) parts)\\n parts))]\\n [(char=? (string-ref path i) #\\\\/)\\n (loop (+ i 1) (+ i 1)\\n (if (> i start)\\n (cons (substring path start i) parts)\\n parts))]\\n [else (loop (+ i 1) start parts)])))\\n\\n(define (mkdir-p path)\\n (when (string-contains-char path #\\\\nul)\\n (error 'mkdir-p \\\"path contains null byte\\\" path))\\n (let ([components (split-path path)])\\n (let loop ([parts components] [current \\\"\\\"])\\n (unless (null? parts)\\n (let ([dir (if (string=? current \\\"\\\")\\n (car parts)\\n (string-append current \\\"/\\\" (car parts)))])\\n (unless (or (string=? dir \\\"\\\") (file-directory? dir))\\n (guard (exn [#t (void)]) ;; ignore EEXIST races\\n (mkdir dir #o755)))\\n (loop (cdr parts) dir))))))") ("id" . "jerboa-safe-mkdir-p") ("imports" "(chezscheme)") ("notes" . "Never use (system ...) with user-controlled paths. The guard around mkdir handles EEXIST from race conditions. Check for null bytes to prevent truncation attacks.") @@ -1609,7 +1612,7 @@ "TLS Connection with Certificate Pinning (Rustls)")) (("code" . - ";; Create a spinlock (optional max-spin count, default 10)\n(define sl (make-spinlock))\n(define sl2 (make-spinlock 20)) ; 20 spins before yield\n\n;; Manual lock/unlock\n(spin-lock! sl)\n;; ... critical section ...\n(spin-unlock! sl)\n\n;; Macro form (exception-safe, auto-unlocks)\n(with-spinlock sl\n (+ 1 2 3)) ; => 6\n\n;; Implementation uses Chez box-cas! for the CAS operation:\n;; (box-cas! box old new) => #t if swapped\n;; Deadlock detection: errors if current thread already holds lock.\n;; Yields via (sleep (make-time 'time-duration 0 0)) after max spins.") ("id" . "chez-spinlock-box-cas") + ";; Create a spinlock (optional max-spin count, default 10)\n(define sl (make-spinlock))\n(define sl2 (make-spinlock 20)) ; 20 spins before yield\n\n;; Manual lock/unlock\n(spin-lock! sl)\n;; ... critical section ...\n(spin-unlock! sl)\n\n;; Macro form (exception-safe, auto-unlocks)\n(with-spinlock sl\n (+ 1 2 3)) ; => 6\n\n;; Implementation uses Chez box-cas! for the CAS operation:\n;; (box-cas! box old new) => #t if swapped\n;; Deadlock detection: errors if current thread already holds lock.\n;; Yields via (sleep (make-time 'time-duration 0 0)) after max spins.") ("id" . "jerboa-spinlock-box-cas") ("imports" "(std misc spinlock)") ("notes" . @@ -1619,7 +1622,7 @@ ("title" . "CAS-based Spinlock using box-cas!")) (("code" . - ";; WRONG: syntax-rules can't reference library-local bindings in templates\n;; (define-syntax my-macro\n;; (syntax-rules ()\n;; ((_ x) (my-helper x)))) ; => \"misplaced aux keyword\" error\n\n;; CORRECT: use syntax-case with lambda wrapper\n(define-syntax my-macro\n (lambda (stx)\n (syntax-case stx ()\n ((_ x)\n #'(my-helper x)))))\n\n;; Multi-clause with rest args:\n(define-syntax with-thing\n (lambda (stx)\n (syntax-case stx ()\n ((_ obj expr)\n #'(let ((o obj))\n (thing-acquire! o)\n (let ((result (guard (e (else (thing-release! o) (raise e)))\n expr)))\n (thing-release! o)\n result)))\n ((_ obj expr rest ...)\n #'(with-thing obj (begin expr rest ...))))))") ("id" . "chez-syntax-case-in-library") + ";; WRONG: syntax-rules can't reference library-local bindings in templates\n;; (define-syntax my-macro\n;; (syntax-rules ()\n;; ((_ x) (my-helper x)))) ; => \"misplaced aux keyword\" error\n\n;; CORRECT: use syntax-case with lambda wrapper\n(define-syntax my-macro\n (lambda (stx)\n (syntax-case stx ()\n ((_ x)\n #'(my-helper x)))))\n\n;; Multi-clause with rest args:\n(define-syntax with-thing\n (lambda (stx)\n (syntax-case stx ()\n ((_ obj expr)\n #'(let ((o obj))\n (thing-acquire! o)\n (let ((result (guard (e (else (thing-release! o) (raise e)))\n expr)))\n (thing-release! o)\n result)))\n ((_ obj expr rest ...)\n #'(with-thing obj (begin expr rest ...))))))") ("id" . "jerboa-syntax-case-in-library") ("imports" "(chezscheme)") ("notes" . @@ -1631,7 +1634,7 @@ "Use syntax-case (not syntax-rules) for macros in R6RS libraries")) (("code" . - ";; Problem: define-record-type auto-generates make-foo, but you want\n;; a custom constructor (e.g., with default arguments via case-lambda).\n;; Using (define make-foo ...) after define-record-type causes\n;; \"multiple definitions\" error.\n\n;; Solution: use 3-name form to give internal constructor a different name\n(define-record-type (%foo %make-foo foo?)\n (nongenerative foo-unique-id)\n (sealed #t)\n (fields (immutable x) (immutable y)))\n\n;; Now define public constructor with defaults\n(define make-foo\n (case-lambda\n (() (%make-foo 0 0))\n ((x) (%make-foo x 0))\n ((x y) (%make-foo x y))))\n\n;; Accessors use the first name: %foo-x, %foo-y\n;; Predicate uses third name: foo?") ("id" . "chez-record-type-custom-constructor") + ";; Problem: define-record-type auto-generates make-foo, but you want\n;; a custom constructor (e.g., with default arguments via case-lambda).\n;; Using (define make-foo ...) after define-record-type causes\n;; \"multiple definitions\" error.\n\n;; Solution: use 3-name form to give internal constructor a different name\n(define-record-type (%foo %make-foo foo?)\n (nongenerative foo-unique-id)\n (sealed #t)\n (fields (immutable x) (immutable y)))\n\n;; Now define public constructor with defaults\n(define make-foo\n (case-lambda\n (() (%make-foo 0 0))\n ((x) (%make-foo x 0))\n ((x y) (%make-foo x y))))\n\n;; Accessors use the first name: %foo-x, %foo-y\n;; Predicate uses third name: foo?") ("id" . "jerboa-record-type-custom-constructor") ("imports" "(chezscheme)") ("notes" . @@ -2132,27 +2135,27 @@ "Use gmake (GNU Make) on FreeBSD for Jerboa projects")) (("code" . - "# The build-binary.ss script needs CHEZ_DIR to find petite.boot,\n# scheme.boot, main.o, and libkernel.a.\n#\n# It auto-detects ~/.local/lib/csv*/ but on FreeBSD, Chez is\n# typically installed to /usr/local/lib/ by the pkg system.\n#\n# Find your Chez install:\nls /usr/local/lib/csv*/\n# e.g. /usr/local/lib/csv10.3.0/ta6fb/\n#\n# Verify it has the required files:\nls /usr/local/lib/csv10.3.0/ta6fb/{main.o,petite.boot,scheme.boot,scheme.h}\n#\n# Build with CHEZ_DIR:\nCHEZ_DIR=/usr/local/lib/csv10.3.0/ta6fb gmake binary") ("id" . "chez-dir-freebsd") ("imports") + "# The build-binary.ss script needs CHEZ_DIR to find petite.boot,\n# scheme.boot, main.o, and libkernel.a.\n#\n# It auto-detects ~/.local/lib/csv*/ but on FreeBSD, Chez is\n# typically installed to /usr/local/lib/ by the pkg system.\n#\n# Find your Chez install:\nls /usr/local/lib/csv*/\n# e.g. /usr/local/lib/csv10.3.0/ta6fb/\n#\n# Verify it has the required files:\nls /usr/local/lib/csv10.3.0/ta6fb/{main.o,petite.boot,scheme.boot,scheme.h}\n#\n# Build with CHEZ_DIR:\nCHEZ_DIR=/usr/local/lib/csv10.3.0/ta6fb gmake binary") ("id" . "jerboa-dir-freebsd") ("imports") ("notes" . - "The build-binary.ss script only auto-detects Chez in ~/.local/lib/csv*/<machine-type>/. On FreeBSD (pkg install chez-scheme), Chez installs to /usr/local/lib/csv<version>/ta6fb/. The machine-type directory (ta6fb = threaded, a6 = amd64, fb = FreeBSD) varies by platform. Set CHEZ_DIR to the directory containing main.o, petite.boot, scheme.boot, and scheme.h.") + "The build-binary.ss script only auto-detects Chez in ~/.local/lib/csv*/<machine-type>/. On FreeBSD (pkg install jerboa-scheme), Chez installs to /usr/local/lib/csv<version>/ta6fb/. The machine-type directory (ta6fb = threaded, a6 = amd64, fb = FreeBSD) varies by platform. Set CHEZ_DIR to the directory containing main.o, petite.boot, scheme.boot, and scheme.h.") ("related" "freebsd-gmake-required") - ("tags" "chez-dir" "freebsd" "binary" "build" "native" + ("tags" "jerboa-dir" "freebsd" "binary" "build" "native" "install-path") ("title" . "Set CHEZ_DIR on FreeBSD for native binary builds")) (("code" . - ";; FreeBSD static binary linking — key differences from Linux musl builds:\n;;\n;; 1. Find Chez static lib (ta6fb, not ta6le):\n;; /usr/local/lib/csv10.3.0/ta6fb/libkernel.a\n;;\n;; 2. Use cc (clang), not musl-gcc:\n;; cc -static -o myapp main.o static_boot.o ffi-shim.o \\\n;; -L/usr/local/lib/csv10.3.0/ta6fb -lkernel \\\n;; -lz -lm -lthr -liconv -lncurses -luuid -llz4 \\\n;; -Wl,--allow-multiple-definition\n;;\n;; 3. __errno_location does not exist on FreeBSD — register a wrapper:\n;; static int *freebsd_errno_location(void) { return &errno; }\n;; Sforeign_symbol(\"__errno_location\", (void*)freebsd_errno_location);\n;;\n;; 4. memfd_create works on FreeBSD 13+ but use /dev/fd/N (not /proc/self/fd/N):\n;; int fd = memfd_create(\"prog\", MFD_CLOEXEC);\n;; snprintf(path, sizeof(path), \"/dev/fd/%d\", fd);\n;;\n;; 5. Exe path resolution via sysctl (no /proc/self/exe):\n;; int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };\n;; sysctl(mib, 4, exe_buf, &exe_len, NULL, 0);\n;;\n;; 6. FreeBSD link libs (no -ldl, use -lthr not -lpthread):\n;; -lz -lm -lthr -liconv -lncurses -luuid -llz4\n;; OpenSSL from base: /usr/lib/libssl.a /usr/lib/libcrypto.a\n;;\n;; 7. FreeBSD sed uses -i '' (not -i without argument):\n;; sed -i '' 's/old/new/g' file.sls\n\n;; Example: find Chez ta6fb directory programmatically\n(define chez-ta6fb\n (let ([dirs (directory-list \"/usr/local/lib\")])\n (let ([csv-dir (find (lambda (d) (string-prefix? \"csv\" d)) dirs)])\n (if csv-dir\n (format \"/usr/local/lib/~a/ta6fb\" csv-dir)\n (error 'build \"Cannot find Chez ta6fb in /usr/local/lib\")))))") ("id" . "freebsd-static-binary-link") + ";; FreeBSD static binary linking — key differences from Linux musl builds:\n;;\n;; 1. Find Chez static lib (ta6fb, not ta6le):\n;; /usr/local/lib/csv10.3.0/ta6fb/libkernel.a\n;;\n;; 2. Use cc (clang), not musl-gcc:\n;; cc -static -o myapp main.o static_boot.o ffi-shim.o \\\n;; -L/usr/local/lib/csv10.3.0/ta6fb -lkernel \\\n;; -lz -lm -lthr -liconv -lncurses -luuid -llz4 \\\n;; -Wl,--allow-multiple-definition\n;;\n;; 3. __errno_location does not exist on FreeBSD — register a wrapper:\n;; static int *freebsd_errno_location(void) { return &errno; }\n;; Sforeign_symbol(\"__errno_location\", (void*)freebsd_errno_location);\n;;\n;; 4. memfd_create works on FreeBSD 13+ but use /dev/fd/N (not /proc/self/fd/N):\n;; int fd = memfd_create(\"prog\", MFD_CLOEXEC);\n;; snprintf(path, sizeof(path), \"/dev/fd/%d\", fd);\n;;\n;; 5. Exe path resolution via sysctl (no /proc/self/exe):\n;; int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };\n;; sysctl(mib, 4, exe_buf, &exe_len, NULL, 0);\n;;\n;; 6. FreeBSD link libs (no -ldl, use -lthr not -lpthread):\n;; -lz -lm -lthr -liconv -lncurses -luuid -llz4\n;; OpenSSL from base: /usr/lib/libssl.a /usr/lib/libcrypto.a\n;;\n;; 7. FreeBSD sed uses -i '' (not -i without argument):\n;; sed -i '' 's/old/new/g' file.sls\n\n;; Example: find Chez ta6fb directory programmatically\n(define jerboa-ta6fb\n (let ([dirs (directory-list \"/usr/local/lib\")])\n (let ([csv-dir (find (lambda (d) (string-prefix? \"csv\" d)) dirs)])\n (if csv-dir\n (format \"/usr/local/lib/~a/ta6fb\" csv-dir)\n (error 'build \"Cannot find Chez ta6fb in /usr/local/lib\")))))") ("id" . "freebsd-static-binary-link") ("imports" "(jerboa build)") ("notes" . "FreeBSD static builds do NOT need musl or glibc-compat shims. The system libc (FreeBSD libc) links statically cleanly. Key gotchas: (1) __errno_location is Linux-specific — must provide a wrapper. (2) /proc/self/fd/ and /proc/self/exe don't exist — use /dev/fd/ and sysctl KERN_PROC_PATHNAME. (3) sed -i requires '' as backup extension argument on FreeBSD. (4) Use -lthr not -lpthread on FreeBSD. (5) No -ldl needed (dlopen is in libc). (6) Chez platform is ta6fb (FreeBSD amd64), not ta6le (Linux).") ("related" - "chez-dir-freebsd" + "jerboa-dir-freebsd" "rust-ffi-static-musl" - "chez-errno-access") + "jerboa-errno-access") ("tags" "freebsd" "static" "binary" "link" "clang" "libkernel" "ta6fb") ("title" @@ -2160,7 +2163,7 @@ "Link a static Chez Scheme binary on FreeBSD (no musl)")) (("code" . - "# FreeBSD requires gmake — BSD make cannot parse lz4's GNU Makefile\n# (produces 'Invalid line \"ifeq ...\"' errors)\npkg install gmake\n\n# Configure with absolute installprefix (relative paths fail)\n./configure --installprefix=/home/user/chez-local --threads\n\n# Build and install using gmake\ngmake -j$(sysctl -n hw.ncpu)\nzuo ta6fb install # or: gmake install (for older workareas)\n\n# With security hardening (--enable-harden):\n./configure --installprefix=/home/user/chez-secure --threads --enable-harden\n# Adds: -fstack-protector-strong -fstack-clash-protection -D_FORTIFY_SOURCE=2\n# -fPIC (CFLAGS), -pie (mdlinkflags only), -Wl,-z,relro,-z,now (LDFLAGS)\n# On x86_64 Linux: also adds -fcf-protection=full (Intel CET)\n# On arm64: also adds -mbranch-protection=standard (ARM PAC+BTI)\n# Note: -pie goes in mdlinkflags, NOT LDFLAGS — LDFLAGS is inherited by\n# ftype tests that compile shared objects and -pie conflicts with -shared\n\n# After configure, if you add --enable-harden and rebuild,\n# you must clean C objects manually (build system may not re-detect flag changes):\nfind ta6fb/c -name \"*.o\" -delete\nrm -f ta6fb/boot/ta6fb/main.o ta6fb/boot/ta6fb/libkernel.a\nrm -rf ta6fb/zlib ta6fb/lz4\ngmake\n\n# Verify hardening on the resulting binary:\nfile ta6fb/bin/ta6fb/scheme # should say \"pie executable\"\nreadelf -d ta6fb/bin/ta6fb/scheme | grep -i \"flags\\|bind_now\"\n# FLAGS: BIND_NOW and FLAGS_1: NOW PIE confirms full RELRO + ASLR\n\n# Machine type on FreeBSD amd64: ta6fb (threaded, a6=amd64, fb=FreeBSD)") ("id" . "chez-freebsd-build-from-source") ("imports") + "# FreeBSD requires gmake — BSD make cannot parse lz4's GNU Makefile\n# (produces 'Invalid line \"ifeq ...\"' errors)\npkg install gmake\n\n# Configure with absolute installprefix (relative paths fail)\n./configure --installprefix=/home/user/jerboa-local --threads\n\n# Build and install using gmake\ngmake -j$(sysctl -n hw.ncpu)\nzuo ta6fb install # or: gmake install (for older workareas)\n\n# With security hardening (--enable-harden):\n./configure --installprefix=/home/user/jerboa-secure --threads --enable-harden\n# Adds: -fstack-protector-strong -fstack-clash-protection -D_FORTIFY_SOURCE=2\n# -fPIC (CFLAGS), -pie (mdlinkflags only), -Wl,-z,relro,-z,now (LDFLAGS)\n# On x86_64 Linux: also adds -fcf-protection=full (Intel CET)\n# On arm64: also adds -mbranch-protection=standard (ARM PAC+BTI)\n# Note: -pie goes in mdlinkflags, NOT LDFLAGS — LDFLAGS is inherited by\n# ftype tests that compile shared objects and -pie conflicts with -shared\n\n# After configure, if you add --enable-harden and rebuild,\n# you must clean C objects manually (build system may not re-detect flag changes):\nfind ta6fb/c -name \"*.o\" -delete\nrm -f ta6fb/boot/ta6fb/main.o ta6fb/boot/ta6fb/libkernel.a\nrm -rf ta6fb/zlib ta6fb/lz4\ngmake\n\n# Verify hardening on the resulting binary:\nfile ta6fb/bin/ta6fb/scheme # should say \"pie executable\"\nreadelf -d ta6fb/bin/ta6fb/scheme | grep -i \"flags\\|bind_now\"\n# FLAGS: BIND_NOW and FLAGS_1: NOW PIE confirms full RELRO + ASLR\n\n# Machine type on FreeBSD amd64: ta6fb (threaded, a6=amd64, fb=FreeBSD)") ("id" . "jerboa-freebsd-build-from-source") ("imports") ("notes" . "lz4's Makefile uses GNU make conditionals (ifeq/else/endif) that BSD make rejects with 'Invalid line' errors. Always use gmake. The --enable-harden flag was added to the secure branch of ChezScheme (~/ChezScheme). The -pie vs -shared conflict: -pie in LDFLAGS breaks ftype tests which compile .so files using the same LDFLAGS; fix is to put -pie in mdlinkflags which is only used for the scheme executable link, not inherited by test compilation. Use -fPIC (not -fPIE) in CFLAGS since ftype tests also inherit CFLAGS and need -fPIC for shared object compilation.") @@ -2209,7 +2212,7 @@ ("imports" "(jerboa prelude)") ("notes" . - "The (jerboa prelude) re-exports `make-time` as a datetime constructor, shadowing the Chez `make-time` that `sleep` requires. The `(let () (import (only (chezscheme) make-time)) make-time)` trick captures the original binding before shadowing occurs — the same technique used in `chez-srfis/srfi/srfi-18.sls`. Note: arg order to `make-time` is `(make-time type nanoseconds seconds)`. Without this workaround, `(sleep (make-time 'time-duration 0 1))` fails because prelude's `make-time` expects a year/month/day signature.") + "The (jerboa prelude) re-exports `make-time` as a datetime constructor, shadowing the Chez `make-time` that `sleep` requires. The `(let () (import (only (chezscheme) make-time)) make-time)` trick captures the original binding before shadowing occurs — the same technique used in `jerboa-srfis/srfi/srfi-18.sls`. Note: arg order to `make-time` is `(make-time type nanoseconds seconds)`. Without this workaround, `(sleep (make-time 'time-duration 0 1))` fails because prelude's `make-time` expects a year/month/day signature.") ("superseded_by" . "sleep-ms-prelude") ("tags" "thread-sleep" "sleep" "make-time" "prelude" "shadow" "duration" "standalone") @@ -2295,11 +2298,11 @@ "Background worker delivering results to jemacs UI thread via ui-queue-push!")) (("code" . - ";; Pattern: vendor/chez-ssl-static.sls\n;; Copy the upstream module's .sls to vendor/, then guard load-shared-object\n;; so the static binary skips it (symbols already registered via Sforeign_symbol).\n\n;; In vendor/chez-ssl-static.sls (library declaration matches original):\n;; (library (chez-ssl) ...)\n;; ...same body as upstream...\n;; BUT replace:\n;; (load-shared-object \"libssl.so\")\n;; (load-shared-object \"libcrypto.so\")\n;; (load-shared-object \"chez_ssl_shim.so\")\n;; WITH:\n\n(define load-libs\n (let ([static (getenv \"JEMACS_STATIC\")])\n (if (and static\n (not (string=? static \"\"))\n (not (string=? static \"0\")))\n (void) ;; static binary: symbols pre-registered, skip load\n (begin\n (load-shared-object \"libssl.so\")\n (load-shared-object \"libcrypto.so\")\n (load-shared-object \"chez_ssl_shim.so\")))))\n\n;; The static binary sets JEMACS_STATIC=1 in the build script.\n;; The override file lives in vendor/ which comes BEFORE the upstream\n;; lib dir in --libdirs, so it shadows the original.\n\n;; In Makefile, add vendor/ to LIBDIRS before the upstream path:\n;; LIBDIRS = vendor:$(CSSL):...\n;;\n;; In Docker container setup, copy the override into place:\n;; cp vendor/chez-ssl-static.sls /deps/chez-ssl/src/chez-ssl.sls") ("id" . "static-vendor-module-override") + ";; Pattern: vendor/jerboa-ssl-static.sls\n;; Copy the upstream module's .sls to vendor/, then guard load-shared-object\n;; so the static binary skips it (symbols already registered via Sforeign_symbol).\n\n;; In vendor/jerboa-ssl-static.sls (library declaration matches original):\n;; (library (jerboa-ssl) ...)\n;; ...same body as upstream...\n;; BUT replace:\n;; (load-shared-object \"libssl.so\")\n;; (load-shared-object \"libcrypto.so\")\n;; (load-shared-object \"jerboa_ssl_shim.so\")\n;; WITH:\n\n(define load-libs\n (let ([static (getenv \"JEMACS_STATIC\")])\n (if (and static\n (not (string=? static \"\"))\n (not (string=? static \"0\")))\n (void) ;; static binary: symbols pre-registered, skip load\n (begin\n (load-shared-object \"libssl.so\")\n (load-shared-object \"libcrypto.so\")\n (load-shared-object \"jerboa_ssl_shim.so\")))))\n\n;; The static binary sets JEMACS_STATIC=1 in the build script.\n;; The override file lives in vendor/ which comes BEFORE the upstream\n;; lib dir in --libdirs, so it shadows the original.\n\n;; In Makefile, add vendor/ to LIBDIRS before the upstream path:\n;; LIBDIRS = vendor:$(CSSL):...\n;;\n;; In Docker container setup, copy the override into place:\n;; cp vendor/jerboa-ssl-static.sls /deps/jerboa-ssl/src/jerboa-ssl.sls") ("id" . "static-vendor-module-override") ("imports" "(chezscheme)") ("notes" . - "Use this when a third-party library calls load-shared-object at module load time and you can't modify the upstream source. The override file is only compiled into the Docker container — development mode still uses the upstream. The JEMACS_STATIC check is a 3-way guard: (and static (not empty) (not \"0\")) so unsetting the var reverts to dynamic loading. Alpine's openssl-dev only ships shared .so files — if you need static libssl.a, you must build OpenSSL from source inside the container. See also: chez-load-shared-object-static for the simpler guard-only pattern.") + "Use this when a third-party library calls load-shared-object at module load time and you can't modify the upstream source. The override file is only compiled into the Docker container — development mode still uses the upstream. The JEMACS_STATIC check is a 3-way guard: (and static (not empty) (not \"0\")) so unsetting the var reverts to dynamic loading. Alpine's openssl-dev only ships shared .so files — if you need static libssl.a, you must build OpenSSL from source inside the container. See also: jerboa-load-shared-object-static for the simpler guard-only pattern.") ("tags" "static" "vendor" "override" "load-shared-object" "JEMACS_STATIC" "build") ("title" @@ -2354,7 +2357,7 @@ "Resolve Import Name Collisions Between Modules Using rename")) (("code" . - "/* In C (ffi-shim.c), the Linux chez_fuse_mount uses fusermount3/fusermount:\n *\n * 1. find_fusermount() - locates /usr/bin/fusermount3 or /usr/bin/fusermount\n * 2. mount_via_fusermount() - creates socketpair, forks, sets _FUSE_COMMFD\n * env var, execs fusermount with -o options, sends /dev/fuse fd via SCM_RIGHTS\n * 3. Falls back to direct mount() syscall for root/CAP_SYS_ADMIN\n *\n * Unmount also tries fusermount -u first, falls back to umount2.\n *\n * The _FUSE_COMMFD protocol: child gets env var with socket fd number,\n * reads the /dev/fuse fd via recvmsg+SCM_RIGHTS from that socket.\n * This is the standard libfuse3 protocol used by fusermount3.\n */") ("id" . "fuse-mount-linux-fusermount") ("imports") + "/* In C (ffi-shim.c), the Linux jerboa_fuse_mount uses fusermount3/fusermount:\n *\n * 1. find_fusermount() - locates /usr/bin/fusermount3 or /usr/bin/fusermount\n * 2. mount_via_fusermount() - creates socketpair, forks, sets _FUSE_COMMFD\n * env var, execs fusermount with -o options, sends /dev/fuse fd via SCM_RIGHTS\n * 3. Falls back to direct mount() syscall for root/CAP_SYS_ADMIN\n *\n * Unmount also tries fusermount -u first, falls back to umount2.\n *\n * The _FUSE_COMMFD protocol: child gets env var with socket fd number,\n * reads the /dev/fuse fd via recvmsg+SCM_RIGHTS from that socket.\n * This is the standard libfuse3 protocol used by fusermount3.\n */") ("id" . "fuse-mount-linux-fusermount") ("imports") ("notes" . "Non-root FUSE mount on Linux requires the fusermount setuid helper. Direct mount() returns EPERM (errno 1) without CAP_SYS_ADMIN. The _FUSE_COMMFD env var protocol is used by both fusermount3 (FUSE 3) and fusermount (FUSE 2). Always fall back to direct mount() for root users or containers.") @@ -2385,7 +2388,7 @@ "defvariant + match-variant are in (std variant), NOT the prelude. match-variant performs exhaustiveness checking at EXPAND TIME — missing variants cause a syntax error, not a runtime error. Use _ or else clause to opt out. Naming convention is strict: type/tag for constructor (NOT make-type-tag), type/tag? for predicate, type/tag-field for accessors. defvariant must appear before match-variant in the same compilation unit for compile-time registration to work.") ("related" "ergo-typed-def" - "chez-record-type-custom-constructor") + "jerboa-record-type-custom-constructor") ("tags" "variant" "defvariant" "match-variant" "sum-type" "tagged-union" "exhaustive" "std/variant") ("title" @@ -2673,7 +2676,7 @@ "Scatter / gather with a timeout (alts!! + timeout + atom)")) (("code" . - ";; Detect macOS via machine-type\n(define macos?\n (let ([mt (symbol->string (machine-type))])\n (or (string=? mt \"ta6osx\") (string=? mt \"a6osx\")\n (string=? mt \"tarm64osx\") (string=? mt \"arm64osx\"))))\n\n;; Find Chez install dir — Homebrew on Apple Silicon uses /opt/homebrew/lib\n(define (find-csv-dir lib-dir mt)\n (let ([csv-dir\n (let lp ([dirs (guard (e [#t '()]) (directory-list lib-dir))])\n (cond\n [(null? dirs) #f]\n [(and (> (string-length (car dirs)) 3)\n (string=? \"csv\" (substring (car dirs) 0 3)))\n (format \"~a/~a/~a\" lib-dir (car dirs) mt)]\n [else (lp (cdr dirs))]))])\n (and csv-dir\n (file-exists? (format \"~a/main.o\" csv-dir))\n csv-dir)))\n\n;; Search order: ~/.local/lib, /usr/local/lib, /opt/homebrew/lib, /usr/lib\n(define chez-dir\n (let ([mt (symbol->string (machine-type))]\n [home (getenv \"HOME\")])\n (or (find-csv-dir (format \"~a/.local/lib\" home) mt)\n (find-csv-dir \"/usr/local/lib\" mt)\n (find-csv-dir \"/opt/homebrew/lib\" mt) ;; Apple Silicon Homebrew\n (find-csv-dir \"/usr/lib\" mt))))\n\n;; macOS link flags — no -ldl or -luuid (both in system libs), needs -liconv\n(define link-libs\n (cond\n [macos? \"-lkernel -llz4 -lz -lm -lpthread -lncurses -liconv\"]\n [else \"-lkernel -llz4 -lz -lm -ldl -lpthread -luuid -lncurses\"]))") ("id" . "macos-static-binary-link") + ";; Detect macOS via machine-type\n(define macos?\n (let ([mt (symbol->string (machine-type))])\n (or (string=? mt \"ta6osx\") (string=? mt \"a6osx\")\n (string=? mt \"tarm64osx\") (string=? mt \"arm64osx\"))))\n\n;; Find Chez install dir — Homebrew on Apple Silicon uses /opt/homebrew/lib\n(define (find-csv-dir lib-dir mt)\n (let ([csv-dir\n (let lp ([dirs (guard (e [#t '()]) (directory-list lib-dir))])\n (cond\n [(null? dirs) #f]\n [(and (> (string-length (car dirs)) 3)\n (string=? \"csv\" (substring (car dirs) 0 3)))\n (format \"~a/~a/~a\" lib-dir (car dirs) mt)]\n [else (lp (cdr dirs))]))])\n (and csv-dir\n (file-exists? (format \"~a/main.o\" csv-dir))\n csv-dir)))\n\n;; Search order: ~/.local/lib, /usr/local/lib, /opt/homebrew/lib, /usr/lib\n(define jerboa-dir\n (let ([mt (symbol->string (machine-type))]\n [home (getenv \"HOME\")])\n (or (find-csv-dir (format \"~a/.local/lib\" home) mt)\n (find-csv-dir \"/usr/local/lib\" mt)\n (find-csv-dir \"/opt/homebrew/lib\" mt) ;; Apple Silicon Homebrew\n (find-csv-dir \"/usr/lib\" mt))))\n\n;; macOS link flags — no -ldl or -luuid (both in system libs), needs -liconv\n(define link-libs\n (cond\n [macos? \"-lkernel -llz4 -lz -lm -lpthread -lncurses -liconv\"]\n [else \"-lkernel -llz4 -lz -lm -ldl -lpthread -luuid -lncurses\"]))") ("id" . "macos-static-binary-link") ("imports" "(chezscheme)") ("notes" . @@ -3320,7 +3323,7 @@ ("imports" "(std db sqlite-native)" "(std stm)") ("notes" . - "Use (std db sqlite-native) NOT (std db sqlite) — the sqlite module is missing (chez-sqlite library not bundled). sqlite-native uses bundled rusqlite with no external libsqlite3 dependency. sqlite-query returns a list of alists where keys are column name strings — use (assoc \"column-name\" row) with string keys, not symbol keys. sqlite-execute auto-detects parameter types: integers, flonums, strings, bytevectors are bound natively; #f binds as NULL. WAL mode is critical for concurrent service workloads — without it, any reader blocks all writers. Fire-and-forget guard pattern ((guard (e [#t (void)]))) ensures a SQLite error never crashes a worker thread processing an event. Flush WAL at shutdown: PRAGMA wal_checkpoint(FULL) blocks until all WAL pages are written back to the main DB file. sqlite-open \":memory:\" for in-memory databases.") + "Use (std db sqlite-native) NOT (std db sqlite) — the sqlite module is missing (jerboa-sqlite library not bundled). sqlite-native uses bundled rusqlite with no external libsqlite3 dependency. sqlite-query returns a list of alists where keys are column name strings — use (assoc \"column-name\" row) with string keys, not symbol keys. sqlite-execute auto-detects parameter types: integers, flonums, strings, bytevectors are bound natively; #f binds as NULL. WAL mode is critical for concurrent service workloads — without it, any reader blocks all writers. Fire-and-forget guard pattern ((guard (e [#t (void)]))) ensures a SQLite error never crashes a worker thread processing an event. Flush WAL at shutdown: PRAGMA wal_checkpoint(FULL) blocks until all WAL pages are written back to the main DB file. sqlite-open \":memory:\" for in-memory databases.") ("tags" "sqlite" "sqlite-native" "wal" "persistence" "stm" "replay" "startup" "std db sqlite-native") ("title" @@ -3449,7 +3452,8 @@ "Correct sort argument order: (sort list comparator)")) (("code" . - "\n;; WRONG: Chez map applies lambda right-to-left when building the result list.\n;; Counter increments from the rightmost element first, so indices are reversed.\n(let ([i 0])\n (map (lambda (x)\n (let ([s (list x i)])\n (set! i (+ i 1))\n s))\n '(a b c)))\n;; => ((a 2) (b 1) (c 0)) ← WRONG: c gets 0 not 2\n\n;; CORRECT: use an explicit left-to-right tail-recursive loop\n(let loop ([lst '(a b c)] [i 0] [acc '()])\n (if (null? lst)\n (reverse acc)\n (loop (cdr lst) (+ i 1)\n (cons (list (car lst) i) acc))))\n;; => ((a 0) (b 1) (c 2)) ← correct\n") ("id" . "chez-map-right-to-left-side-effects") ("imports") + "\n;; WRONG: Chez map applies lambda right-to-left when building the result list.\n;; Counter increments from the rightmost element first, so indices are reversed.\n(let ([i 0])\n (map (lambda (x)\n (let ([s (list x i)])\n (set! i (+ i 1))\n s))\n '(a b c)))\n;; => ((a 2) (b 1) (c 0)) ← WRONG: c gets 0 not 2\n\n;; CORRECT: use an explicit left-to-right tail-recursive loop\n(let loop ([lst '(a b c)] [i 0] [acc '()])\n (if (null? lst)\n (reverse acc)\n (loop (cdr lst) (+ i 1)\n (cons (list (car lst) i) acc))))\n;; => ((a 0) (b 1) (c 2)) ← correct\n") ("id" . "jerboa-map-right-to-left-side-effects") + ("imports") ("notes" . "Chez Scheme's (map f lst) internally recurses on the cdr before calling f on car, so side effects (set!) run from right to left even though the result list is in left-to-right order. This bit us hard in streaming aggregation slot index computation: agg-spec slots (agg count 0) (agg avg 1) were emitted as (agg count 1) (agg avg 0) because avg (rightmost) got ai=0. Fix: always use an explicit loop with acc+reverse when you need left-to-right evaluation order with counters.") @@ -3471,7 +3475,7 @@ "Export jerboa-db datoms to DuckDB for OLAP queries")) (("code" . - ";; PROBLEM: Docker Chez Scheme csv10 rejects two token forms even with\n;; #!chezscheme mode prefix:\n;; #\\escape → \"invalid character name\" (use #\\esc instead)\n;; #!void → \"invalid syntax #!void\" (use (void) instead)\n;;\n;; SOLUTION: Text-preprocess the source string before handing it to\n;; (with-input-from-string src read) in jerbuild.\n\n(def (normalize-chez-reader-tokens src)\n ;; Replace #!void with (void) — exact token, word boundary via space/paren\n (let* ([s (re-replace-all (re \"#!void\") src \"(void)\")]\n ;; Replace #\\escape with #\\esc\n [s (re-replace-all (re \"#\\\\\\\\escape\") s \"#\\\\esc\")])\n s))\n\n;; Usage in jerbuild's read loop:\n(def (read-forms-from-string src)\n (let ([normalized (normalize-chez-reader-tokens src)])\n (with-input-from-string normalized\n (lambda ()\n (let loop ([forms '()])\n (let ([form (read)])\n (if (eof-object? form)\n (reverse forms)\n (loop (cons form forms)))))))))") ("id" . "jerbuild-chez-reader-char-void-normalize") + ";; PROBLEM: Docker Chez Scheme csv10 rejects two token forms even with\n;; #!chezscheme mode prefix:\n;; #\\escape → \"invalid character name\" (use #\\esc instead)\n;; #!void → \"invalid syntax #!void\" (use (void) instead)\n;;\n;; SOLUTION: Text-preprocess the source string before handing it to\n;; (with-input-from-string src read) in jerbuild.\n\n(def (normalize-jerboa-reader-tokens src)\n ;; Replace #!void with (void) — exact token, word boundary via space/paren\n (let* ([s (re-replace-all (re \"#!void\") src \"(void)\")]\n ;; Replace #\\escape with #\\esc\n [s (re-replace-all (re \"#\\\\\\\\escape\") s \"#\\\\esc\")])\n s))\n\n;; Usage in jerbuild's read loop:\n(def (read-forms-from-string src)\n (let ([normalized (normalize-jerboa-reader-tokens src)])\n (with-input-from-string normalized\n (lambda ()\n (let loop ([forms '()])\n (let ([form (read)])\n (if (eof-object? form)\n (reverse forms)\n (loop (cons form forms)))))))))") ("id" . "jerbuild-jerboa-reader-char-void-normalize") ("imports") ("notes" . @@ -3548,7 +3552,7 @@ "Start a 2-node TCP Raft cluster with staged peer registration")) (("code" . - ";; PROBLEM: Chez Scheme's (random n) is NOT thread-safe.\n;; Two threads that call (random n) concurrently can read the same PRNG state\n;; and return identical values — even with large n.\n;;\n;; This causes ~50% failure rate when two nodes start simultaneously and both\n;; compute election timeouts from (+ 150 (random 150)):\n;; Thread A: (random 150) → 73 → timeout = 223ms\n;; Thread B: (random 150) → 73 → timeout = 223ms (same! → split vote every time)\n;;\n;; FIX: Add a deterministic, identity-derived constant offset so different nodes\n;; always get different base timeouts regardless of PRNG state:\n\n(define (node-id-hash id)\n (cond\n [(integer? id) id]\n [(symbol? id)\n (let* ([s (symbol->string id)] [n (string-length s)])\n (let loop ([i 0] [h 0])\n (if (= i n) h\n (loop (+ i 1) (+ (* h 31) (char->integer (string-ref s i)))))))]\n [else 0]))\n\n;; Usage: each node gets a unique base, random adds further jitter\n(define (election-timeout-ms node-id)\n ;; 150ms base + 0-74ms id-hash + 0-74ms random = 150-298ms total\n ;; Guaranteed non-equal for different node-ids even if random returns same value\n (+ 150\n (modulo (node-id-hash node-id) 75)\n (random 75)))") ("id" . "chez-random-not-thread-safe") ("imports") + ";; PROBLEM: Chez Scheme's (random n) is NOT thread-safe.\n;; Two threads that call (random n) concurrently can read the same PRNG state\n;; and return identical values — even with large n.\n;;\n;; This causes ~50% failure rate when two nodes start simultaneously and both\n;; compute election timeouts from (+ 150 (random 150)):\n;; Thread A: (random 150) → 73 → timeout = 223ms\n;; Thread B: (random 150) → 73 → timeout = 223ms (same! → split vote every time)\n;;\n;; FIX: Add a deterministic, identity-derived constant offset so different nodes\n;; always get different base timeouts regardless of PRNG state:\n\n(define (node-id-hash id)\n (cond\n [(integer? id) id]\n [(symbol? id)\n (let* ([s (symbol->string id)] [n (string-length s)])\n (let loop ([i 0] [h 0])\n (if (= i n) h\n (loop (+ i 1) (+ (* h 31) (char->integer (string-ref s i)))))))]\n [else 0]))\n\n;; Usage: each node gets a unique base, random adds further jitter\n(define (election-timeout-ms node-id)\n ;; 150ms base + 0-74ms id-hash + 0-74ms random = 150-298ms total\n ;; Guaranteed non-equal for different node-ids even if random returns same value\n (+ 150\n (modulo (node-id-hash node-id) 75)\n (random 75)))") ("id" . "jerboa-random-not-thread-safe") ("imports") ("notes" . "This pattern is critical for any distributed algorithm where multiple nodes start simultaneously and need randomized timeouts that must not collide. Without it, 2-node Raft clusters exhibit ~50% election failure rate.\n\nThe fix works because the id-hash component is a compile-time constant per node — no PRNG involved — so even if (random 75) returns the same value for both threads, the total timeouts differ by the hash offset.\n\nFor truly thread-safe random, you could use a per-thread PRNG seeded with the thread ID, but the hash-offset approach is simpler and sufficient for most distributed timer use cases.") @@ -3559,7 +3563,7 @@ "Chez (random n) is not thread-safe — add node-specific jitter for concurrent timers")) (("code" . - ";; For TCP/multi-process clusters, each node applies from its OWN raft-node log.\n;; (Contrast with in-process clusters which read the LEADER's log directly.)\n;;\n;; replication-for-each-committed! iterates newly committed entries beyond\n;; last-applied-index and calls (proc entry-index tx-ops) for each tx entry.\n;; It also advances the watermark (last-applied-index) before each call —\n;; at-most-once semantics so a crash mid-apply still moves forward.\n\n(define (start-apply-fiber! conn state)\n (fork-thread\n (lambda ()\n (let loop ()\n (sleep (chez-make-time 'time-duration 50000000 0)) ;; 50ms poll\n (when (replication-running? state)\n (replication-for-each-committed! state\n (lambda (entry-index tx-ops)\n (guard (exn [#t\n (display\n (string-append \"apply: skip tx at index \"\n (number->string entry-index) \"\\n\"))])\n (transact! conn tx-ops))))\n (loop))))))\n\n;; NOTE: This requires the (std raft) AppendEntries bug fix:\n;; The follower's log handler must use (<= entry-index prev-idx) when keeping\n;; existing entries — NOT (< entry-index prev-idx). The < version drops the\n;; verified pivot entry on every RPC, leaving followers with only the single\n;; most-recent log entry and causing \"Unknown attribute in pattern\" errors\n;; when earlier schema transactions are missing from the replay.") ("id" . "raft-apply-fiber-per-node-log") + ";; For TCP/multi-process clusters, each node applies from its OWN raft-node log.\n;; (Contrast with in-process clusters which read the LEADER's log directly.)\n;;\n;; replication-for-each-committed! iterates newly committed entries beyond\n;; last-applied-index and calls (proc entry-index tx-ops) for each tx entry.\n;; It also advances the watermark (last-applied-index) before each call —\n;; at-most-once semantics so a crash mid-apply still moves forward.\n\n(define (start-apply-fiber! conn state)\n (fork-thread\n (lambda ()\n (let loop ()\n (sleep (jerboa-make-time 'time-duration 50000000 0)) ;; 50ms poll\n (when (replication-running? state)\n (replication-for-each-committed! state\n (lambda (entry-index tx-ops)\n (guard (exn [#t\n (display\n (string-append \"apply: skip tx at index \"\n (number->string entry-index) \"\\n\"))])\n (transact! conn tx-ops))))\n (loop))))))\n\n;; NOTE: This requires the (std raft) AppendEntries bug fix:\n;; The follower's log handler must use (<= entry-index prev-idx) when keeping\n;; existing entries — NOT (< entry-index prev-idx). The < version drops the\n;; verified pivot entry on every RPC, leaving followers with only the single\n;; most-recent log entry and causing \"Unknown attribute in pattern\" errors\n;; when earlier schema transactions are missing from the replay.") ("id" . "raft-apply-fiber-per-node-log") ("imports" "(jerboa-db replication)" "(jerboa-db core)") ("notes" . @@ -3819,7 +3823,7 @@ "Use __collect_safe for any blocking foreign-procedure (recv, accept, lock, sleep)")) (("code" . - ";; Round 12 / 2026-04-26 — new Chez core primitives.\n;; All are available via (import (chezscheme)) — no extra library needed.\n\n;; ---- bytevector-slice (Phase 67) ----\n;; Extract a sub-range [start, end) — replaces the old bv-sub helper.\n(bytevector-slice #u8(0 1 2 3 4) 1 4) ;; => #u8(1 2 3)\n\n;; ---- bytevector-append (Phase 67) ----\n;; Variadic concatenation — no more local shim needed.\n(bytevector-append #u8(1 2) #u8(3 4) #u8(5)) ;; => #u8(1 2 3 4 5)\n\n;; ---- base64-encode / base64-decode (Phase 66) ----\n;; Core prim: encode a bytevector to standard base64 string.\n(base64-encode (string->utf8 \"Hello\")) ;; => \"SGVsbG8=\"\n(base64-decode \"SGVsbG8=\") ;; => #u8(72 101 108 108 111)\n\n;; ---- sha256-bytevector / sha1-bytevector (Phase 67) ----\n;; Returns raw bytevector (not hex). Use bv->hex to get a hex string.\n(sha256-bytevector (string->utf8 \"hello\"))\n;; => #u8(44 242 77 186 ...) (32-byte SHA-256 digest)\n(sha1-bytevector (string->utf8 \"hello\"))\n;; => #u8(...) (20-byte SHA-1 digest)\n\n;; Quick hex converter:\n(define (bv->hex bv)\n (apply string-append\n (map (lambda (b)\n (let ([s (number->string b 16)])\n (if (= (string-length s) 1) (string-append \"0\" s) s)))\n (bytevector->u8-list bv))))\n\n(bv->hex (sha256-bytevector (string->utf8 \"hello\")))\n;; => \"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\"\n\n;; ---- record->alist (Phase 72) ----\n;; Converts a record to ((field-name . value) ...) walking the full\n;; parent chain, parents first. Replaces manual rtd-walking code.\n(define-record-type point (fields x y))\n(record->alist (make-point 3 4)) ;; => ((x . 3) (y . 4))\n\n;; With inheritance:\n(define-record-type colored-point (parent point) (fields color))\n(record->alist (make-colored-point 1 2 'red))\n;; => ((x . 1) (y . 2) (color . red)) ;; parent fields first") ("id" . "chez-round12-core-prims") + ";; Round 12 / 2026-04-26 — new Chez core primitives.\n;; All are available via (import (chezscheme)) — no extra library needed.\n\n;; ---- bytevector-slice (Phase 67) ----\n;; Extract a sub-range [start, end) — replaces the old bv-sub helper.\n(bytevector-slice #u8(0 1 2 3 4) 1 4) ;; => #u8(1 2 3)\n\n;; ---- bytevector-append (Phase 67) ----\n;; Variadic concatenation — no more local shim needed.\n(bytevector-append #u8(1 2) #u8(3 4) #u8(5)) ;; => #u8(1 2 3 4 5)\n\n;; ---- base64-encode / base64-decode (Phase 66) ----\n;; Core prim: encode a bytevector to standard base64 string.\n(base64-encode (string->utf8 \"Hello\")) ;; => \"SGVsbG8=\"\n(base64-decode \"SGVsbG8=\") ;; => #u8(72 101 108 108 111)\n\n;; ---- sha256-bytevector / sha1-bytevector (Phase 67) ----\n;; Returns raw bytevector (not hex). Use bv->hex to get a hex string.\n(sha256-bytevector (string->utf8 \"hello\"))\n;; => #u8(44 242 77 186 ...) (32-byte SHA-256 digest)\n(sha1-bytevector (string->utf8 \"hello\"))\n;; => #u8(...) (20-byte SHA-1 digest)\n\n;; Quick hex converter:\n(define (bv->hex bv)\n (apply string-append\n (map (lambda (b)\n (let ([s (number->string b 16)])\n (if (= (string-length s) 1) (string-append \"0\" s) s)))\n (bytevector->u8-list bv))))\n\n(bv->hex (sha256-bytevector (string->utf8 \"hello\")))\n;; => \"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\"\n\n;; ---- record->alist (Phase 72) ----\n;; Converts a record to ((field-name . value) ...) walking the full\n;; parent chain, parents first. Replaces manual rtd-walking code.\n(define-record-type point (fields x y))\n(record->alist (make-point 3 4)) ;; => ((x . 3) (y . 4))\n\n;; With inheritance:\n(define-record-type colored-point (parent point) (fields color))\n(record->alist (make-colored-point 1 2 'red))\n;; => ((x . 1) (y . 2) (color . red)) ;; parent fields first") ("id" . "jerboa-round12-core-prims") ("imports" "(chezscheme)") ("notes" . @@ -3827,7 +3831,7 @@ ("related" "ordered-hashtable-insertion-order" "base64-url-safe-unpadded") - ("tags" "chez-core" "round12" "bytevector-slice" + ("tags" "jerboa-core" "round12" "bytevector-slice" "record->alist" "base64" "sha256" "bytevector-append") ("title" . @@ -3840,7 +3844,7 @@ . "Backed by Chez core ordered-hashtable (Phase 68, Round 12 — 2026-04-26). Keys preserve insertion order across all mutating operations.\n\nIMPORTANT: ordered-hashtable? returns #t for these, but plain hashtable? returns #f. Do NOT pass to APIs that expect R6RS hashtables (hashtable-ref, hash-ref, etc.) — they will error. Only use the ordered-hashtable-* procedures.\n\nUse cases: HTTP header tables (order matters for some servers), JSON object preservation (JSON spec doesn't require order, but many APIs rely on it), YAML mappings, LRU queues, deterministic test fixtures.\n\nalist->ordered-hashtable defaults to string-hash + string=? when no hashfn/equiv are passed. To use symbol keys: (alist->ordered-hashtable alist equal-hash eq?).\n\nordered-hashtable-cells is like entries but returns a list of (key . value) pairs instead of two vectors — convenient but allocates more.\n\nCompared to (std clojure) persistent maps: persistent maps are immutable and structurally shared; ordered-hashtable is mutable like a regular Chez hashtable. Use ordered-hashtable when you need mutation and insertion-order semantics together.") ("related" - "chez-round12-core-prims" + "jerboa-round12-core-prims" "hash-table-creation-access") ("tags" "ordered-hashtable" "insertion-order" "http-headers" "json-objects" "deterministic") @@ -3854,7 +3858,7 @@ ("notes" . "The (std text base64) module wraps a portable pure-Scheme RFC 4648 implementation that works on any Chez >= 9.5, regardless of whether base64-encode is in the core boot files. This is important for static builds where the container's Chez boot files predate Phase 66.\n\nThe Chez core base64-encode (no import needed, just (chezscheme)) takes only ONE argument: (base64-encode bv) → standard padded base64 string. If you need url-safe or unpadded, import (std text base64) instead.\n\nTypical usage patterns:\n- JWT: url-safe? = #t, pad? = #f (spec requires this)\n- OpenSSH known_hosts fingerprints: url-safe? = #f, pad? = #f \n- Standard HTTP/MIME: url-safe? = #f, pad? = #t (default)\n\nbase64-decode is not url-safe aware — if decoding a url-safe string, replace - → + and _ → / before calling it.") - ("related" "chez-round12-core-prims") + ("related" "jerboa-round12-core-prims") ("tags" "base64" "url-safe" "unpadded" "jwt" "openssh" "encoding") ("title" @@ -4042,7 +4046,7 @@ ("notes" . "Single-byte tags are the common case (OATH, MGM, OTP). Multi-byte tags appear in OpenPGP card DOs (0x5F52 historical bytes, 0x7F21 cardholder certificate, 0x7F49 public key template) and in X.509 / PKCS / CMS structures. The detection rule is mask 0x1F == 0x1F on the first byte — anything else is a single-byte tag and you must NOT try to read continuation bytes.\n\nIndefinite-length form (length byte == 0x80) is BER but not DER, and smartcards never use it. Reject it explicitly rather than parsing forever — a tampered tag that injects 0x80 would otherwise hang the parser.\n\nWhen GET_DATA returns a composite DO like OpenPGP 0x6E (Application Related Data) or 0x65 (Cardholder Related Data), the response *includes* the outer tag. You must unwrap one level before iterating the inner TLVs — top-level tlv-parse will otherwise just see one big TLV pair.") - ("related" "chez-round12-core-prims") + ("related" "jerboa-round12-core-prims") ("tags" "ber" "tlv" "asn.1" "x.690" "smartcard" "iso7816") ("title" . @@ -4066,9 +4070,9 @@ ("imports" "(std os errno)") ("notes" . - "Supersedes the raw-FFI approach in chez-errno-access. Works cross-platform: Linux (glibc/musl), macOS, FreeBSD, OpenBSD, NetBSD. Constants like EAGAIN, ETIMEDOUT, ENOSYS have different numeric values on BSD vs Linux — always use the named constants, never hard-code the number. The module tries __errno_location, __errno, and __error in platform-priority order. Call (errno) immediately after the foreign call since any Scheme allocation may invoke GC and overwrite errno.") + "Supersedes the raw-FFI approach in jerboa-errno-access. Works cross-platform: Linux (glibc/musl), macOS, FreeBSD, OpenBSD, NetBSD. Constants like EAGAIN, ETIMEDOUT, ENOSYS have different numeric values on BSD vs Linux — always use the named constants, never hard-code the number. The module tries __errno_location, __errno, and __error in platform-priority order. Call (errno) immediately after the foreign call since any Scheme allocation may invoke GC and overwrite errno.") ("related" - "chez-errno-access" + "jerboa-errno-access" "fiber-httpd-sigterm-graceful-shutdown") ("tags" "errno" "posix" "ffi" "cross-platform" "std os errno" "eintr" "eagain") @@ -4133,14 +4137,14 @@ ("title" . "Copy Text to Wayland Clipboard with wl-copy")) (("code" . - ";; Compile dependencies before dependents, and pass make-boot-file the same order.\n;; If (app core) imports (app runtime), runtime must be compiled and embedded first.\n\n(define boot-modules\n '(\"app/runtime\"\n \"std/pregexp\"\n \"std/misc/string\"\n \"app/core\"\n \"app/main\"))\n\n(for-each\n (lambda (m)\n (let ([sls (format \"~a/~a.sls\" lib-dir m)]\n [so (format \"~a/~a.so\" lib-dir m)])\n (when (and (file-exists? sls) (not (file-exists? so)))\n (compile-library sls))))\n boot-modules)\n\n(apply make-boot-file \"app.boot\" '(\"scheme\" \"petite\")\n (map (lambda (m) (format \"~a/~a.so\" lib-dir m)) boot-modules))") ("id" . "chez-boot-file-compilation-instance-order") + ";; Compile dependencies before dependents, and pass make-boot-file the same order.\n;; If (app core) imports (app runtime), runtime must be compiled and embedded first.\n\n(define boot-modules\n '(\"app/runtime\"\n \"std/pregexp\"\n \"std/misc/string\"\n \"app/core\"\n \"app/main\"))\n\n(for-each\n (lambda (m)\n (let ([sls (format \"~a/~a.sls\" lib-dir m)]\n [so (format \"~a/~a.so\" lib-dir m)])\n (when (and (file-exists? sls) (not (file-exists? so)))\n (compile-library sls))))\n boot-modules)\n\n(apply make-boot-file \"app.boot\" '(\"scheme\" \"petite\")\n (map (lambda (m) (format \"~a/~a.so\" lib-dir m)) boot-modules))") ("id" . "jerboa-boot-file-compilation-instance-order") ("imports" "(chezscheme)") ("notes" . "If a static binary fails with \"compiled (X) requires a different compilation instance of (Y)\", check whether Y was compiled after X or embedded after X. Clean stale .so/.wpo files, precompile transitive dependencies first, and preserve that order in make-boot-file. This is separate from the \"library not found\" failure: all libraries may be present but loaded as incompatible instances.") ("related" - "chez-boot-file-missing-library" - "chez-stale-wpo-build-failure") + "jerboa-boot-file-missing-library" + "jerboa-stale-wpo-build-failure") ("tags" "chez" "boot" "make-boot-file" "fasl" "compilation-instance" "static-binary") ("title" @@ -4172,7 +4176,7 @@ "Resolve jerbuild auto-import conflicts between (jerboa core) and (jerboa runtime)")) (("code" . - "# Before building a binary for a different Chez machine type, remove compiled caches\n# from every library root that can be embedded in the boot image.\nfor d in src/jsh src/compat vendor/jerboa/lib vendor/chez-ssh/src \\\n vendor/chez-sqlite/src vendor/chez-crypto/src \\\n vendor/jerboa-coreutils/lib vendor/jerboa-awk/lib \\\n vendor/jerboa-sed/lib vendor/jerboa-aws/lib vendor/chez-fuse/lib; do\n [ -d \"$d\" ] && find \"$d\" \\( -name '*.so' -o -name '*.wpo' \\) -delete\ndone\nrm -f jsh-generated.so jsh-generated.wpo jsh.boot jsh-libs.boot") ("id" . "clean-cross-target-fasl-caches") ("imports") + "# Before building a binary for a different Chez machine type, remove compiled caches\n# from every library root that can be embedded in the boot image.\nfor d in src/jsh src/compat vendor/jerboa/lib vendor/jerboa-ssh/src \\\n vendor/jerboa-sqlite/src vendor/jerboa-crypto/src \\\n vendor/jerboa-coreutils/lib vendor/jerboa-awk/lib \\\n vendor/jerboa-sed/lib vendor/jerboa-aws/lib vendor/jerboa-fuse/lib; do\n [ -d \"$d\" ] && find \"$d\" \\( -name '*.so' -o -name '*.wpo' \\) -delete\ndone\nrm -f jsh-generated.so jsh-generated.wpo jsh.boot jsh-libs.boot") ("id" . "clean-cross-target-fasl-caches") ("imports") ("notes" . "A Linux cross-build can leave ta6le/tarm64le FASLs in vendored library trees. A later macOS build may embed those stale files and fail at runtime with \"incompatible fasl-object machine-type\" or compilation-instance errors. Clean all library roots that make-boot-file can consume, not only project-local src/.") @@ -4183,7 +4187,7 @@ "Clean .so/.wpo caches when switching Chez machine targets")) (("code" . - "(import (chezscheme))\n\n;; C side example:\n;; typedef int (*app_fn)(int, const char **);\n;; int call_app(uintptr_t fn_addr, int argc, const char **argv) {\n;; if (fn_addr == 0) return -1;\n;; app_fn fn = (app_fn)fn_addr;\n;; return fn(argc, argv);\n;; }\n\n(define call-app\n (foreign-procedure \"call_app\" (uptr int void*) int))\n\n(define (symbol-entry-or-zero name)\n (guard (e [#t 0])\n (foreign-entry name)))\n\n(let ([entry (symbol-entry-or-zero \"registered_c_symbol\")])\n (if (= entry 0)\n 127\n (call-app entry 0 0)))") ("id" . "chez-foreign-entry-function-pointer") + "(import (chezscheme))\n\n;; C side example:\n;; typedef int (*app_fn)(int, const char **);\n;; int call_app(uintptr_t fn_addr, int argc, const char **argv) {\n;; if (fn_addr == 0) return -1;\n;; app_fn fn = (app_fn)fn_addr;\n;; return fn(argc, argv);\n;; }\n\n(define call-app\n (foreign-procedure \"call_app\" (uptr int void*) int))\n\n(define (symbol-entry-or-zero name)\n (guard (e [#t 0])\n (foreign-entry name)))\n\n(let ([entry (symbol-entry-or-zero \"registered_c_symbol\")])\n (if (= entry 0)\n 127\n (call-app entry 0 0)))") ("id" . "jerboa-foreign-entry-function-pointer") ("imports" "(chezscheme)") ("notes" . @@ -4253,17 +4257,17 @@ "Disassemble raw bytevectors with (std debug disassemble)")) (("code" . - "#!chezscheme\n(import (chezscheme)\n (rename (std net request)\n (http-get raw-http-get)\n (http-post raw-http-post)))\n\n(define (kw-ref args key default)\n (let loop ([rest args])\n (cond\n [(null? rest) default]\n [(and (pair? (cdr rest)) (eq? (car rest) key)) (cadr rest)]\n [else (loop (cdr rest))])))\n\n(define (header-value h)\n (cond\n [(and (pair? h) (pair? (cdr h)) (eq? (cadr h) '::) (pair? (cddr h)))\n (caddr h)]\n [(pair? h) (cdr h)]\n [else \"\"]))\n\n(define (normalize-header h)\n (cons (car h)\n (let ([v (header-value h)])\n (if (string? v) v (format \"~a\" v)))))\n\n(define (normalize-headers headers)\n (let loop ([rest headers] [out '()])\n (cond\n [(null? rest) (reverse out)]\n [else\n (let ([h (normalize-header (car rest))])\n ;; std/net/request writes Host itself; do not duplicate it for SigV4.\n (loop (cdr rest)\n (if (and (string? (car h)) (string-ci=? (car h) \"Host\"))\n out\n (cons h out))))])))\n\n(define (request-args args)\n (values (normalize-headers (kw-ref args 'headers: '()))\n (kw-ref args 'data: #f)))\n\n(define (http-get url . args)\n (let-values ([(headers data) (request-args args)])\n (raw-http-get url headers data)))\n\n(define (http-post url . args)\n (let-values ([(headers data) (request-args args)])\n (raw-http-post url headers (or data \"\"))))") ("id" . "chez-https-to-std-net-request-shim") + "#!chezscheme\n(import (chezscheme)\n (rename (std net request)\n (http-get raw-http-get)\n (http-post raw-http-post)))\n\n(define (kw-ref args key default)\n (let loop ([rest args])\n (cond\n [(null? rest) default]\n [(and (pair? (cdr rest)) (eq? (car rest) key)) (cadr rest)]\n [else (loop (cdr rest))])))\n\n(define (header-value h)\n (cond\n [(and (pair? h) (pair? (cdr h)) (eq? (cadr h) '::) (pair? (cddr h)))\n (caddr h)]\n [(pair? h) (cdr h)]\n [else \"\"]))\n\n(define (normalize-header h)\n (cons (car h)\n (let ([v (header-value h)])\n (if (string? v) v (format \"~a\" v)))))\n\n(define (normalize-headers headers)\n (let loop ([rest headers] [out '()])\n (cond\n [(null? rest) (reverse out)]\n [else\n (let ([h (normalize-header (car rest))])\n ;; std/net/request writes Host itself; do not duplicate it for SigV4.\n (loop (cdr rest)\n (if (and (string? (car h)) (string-ci=? (car h) \"Host\"))\n out\n (cons h out))))])))\n\n(define (request-args args)\n (values (normalize-headers (kw-ref args 'headers: '()))\n (kw-ref args 'data: #f)))\n\n(define (http-get url . args)\n (let-values ([(headers data) (request-args args)])\n (raw-http-get url headers data)))\n\n(define (http-post url . args)\n (let-values ([(headers data) (request-args args)])\n (raw-http-post url headers (or data \"\"))))") ("id" . "jerboa-https-to-std-net-request-shim") ("imports" "(chezscheme)" "(std net request)") ("notes" . - "Use this when porting code that called chez-https as (http-get url 'headers: headers) or (http-post url 'headers: headers 'data: body). (std net request) does not consume those keyword arguments; pass headers and body positionally. It also expects (\"Name\" . \"value\") headers, not chez-https-style (\"Name\" :: \"value\") lists. For AWS SigV4, drop the explicit Host header before sending because std/net/request writes the Host line itself; duplicate Host headers can change the canonical request seen by AWS. Confirmed in jerboa-shell on May 20, 2026: missing translation caused unauthenticated S3 root requests to return HTTP 307, while the shim produced signed AWS 403 InvalidAccessKeyId with fake credentials.") + "Use this when porting code that called jerboa-https as (http-get url 'headers: headers) or (http-post url 'headers: headers 'data: body). (std net request) does not consume those keyword arguments; pass headers and body positionally. It also expects (\"Name\" . \"value\") headers, not jerboa-https-style (\"Name\" :: \"value\") lists. For AWS SigV4, drop the explicit Host header before sending because std/net/request writes the Host line itself; duplicate Host headers can change the canonical request seen by AWS. Confirmed in jerboa-shell on May 20, 2026: missing translation caused unauthenticated S3 root requests to return HTTP 307, while the shim produced signed AWS 403 InvalidAccessKeyId with fake credentials.") ("related" "http-client") - ("tags" "http" "headers" "chez-https" "std-net-request" + ("tags" "http" "headers" "jerboa-https" "std-net-request" "compatibility" "aws") ("title" . - "Wrap chez-https-style calls around std net request")) + "Wrap jerboa-https-style calls around std net request")) (("code" . "(import (jerboa rust codegen))\n\n(define program\n '((module calc)\n (define (add-one (x i32)) -> i32\n (+ x 1))\n (define (abs-i32 (x i32)) -> i32\n (if (< x 0) (- 0 x) x))\n (define (square-sum (x i32) (y i32)) -> i32\n (let ([sum (+ x y)])\n (* sum sum)))))\n\n(display (safe-rust-program->string program))\n;; Or write it directly:\n;; (write-safe-rust-program program \"calc.rs\")") ("id" . "safe-rust-codegen-target") @@ -4415,7 +4419,7 @@ "Platform-aware RLIMIT_* numeric codes for setrlimit")) (("code" . - ";; supervise / launch-spec's child-pre-exec hook runs in the forked child BEFORE\n;; execve. The Chez runtime is in a fragile state there: do not allocate via the\n;; collector, do not open or close Chez ports, do not use dynamic-wind. Any of\n;; those can deadlock or crash the child with no usable stderr.\n\n;; SAFE in child-pre-exec:\n;; - foreign-procedure calls (setrlimit, sandbox_init, prctl, chdir, setenv)\n;; - raw syscall wrappers\n;; - assq / car / cdr on a closed-over alist captured pre-fork\n\n;; UNSAFE in child-pre-exec:\n;; - (display ...) / (write ...) / (format ...) to any port\n;; - (open-output-file ...) / port operations\n;; - dynamic-wind / parameterize (re-entry hits the broken runtime)\n;; - large allocations / make-vector of significant size\n;; - guard / with-exception-handler beyond a thin try/catch wrapper\n\n;; If you need to log from the child, write(2) directly to fd 2 via a\n;; foreign-procedure, not a Chez port.") ("id" . "supervise-child-pre-exec-no-chez-io") + ";; supervise / launch-spec's child-pre-exec hook runs in the forked child BEFORE\n;; execve. The Chez runtime is in a fragile state there: do not allocate via the\n;; collector, do not open or close Chez ports, do not use dynamic-wind. Any of\n;; those can deadlock or crash the child with no usable stderr.\n\n;; SAFE in child-pre-exec:\n;; - foreign-procedure calls (setrlimit, sandbox_init, prctl, chdir, setenv)\n;; - raw syscall wrappers\n;; - assq / car / cdr on a closed-over alist captured pre-fork\n\n;; UNSAFE in child-pre-exec:\n;; - (display ...) / (write ...) / (format ...) to any port\n;; - (open-output-file ...) / port operations\n;; - dynamic-wind / parameterize (re-entry hits the broken runtime)\n;; - large allocations / make-vector of significant size\n;; - guard / with-exception-handler beyond a thin try/catch wrapper\n\n;; If you need to log from the child, write(2) directly to fd 2 via a\n;; foreign-procedure, not a Chez port.") ("id" . "supervise-child-pre-exec-no-jerboa-io") ("imports" "(std os supervise)") ("notes" . @@ -4534,12 +4538,12 @@ "Limit MCP security scan output to changed lines")) (("code" . - ";; #!chezscheme library files are verified with the Chez reader path:\n;; {\"name\":\"jerboa_verify\",\n;; \"arguments\":{\"file_path\":\"lib/std/ergo.ss\"}}\n\n;; Lint fixed-arity FFI declarations for known variadic libc symbols:\n;; {\"name\":\"jerboa_variadic_ffi_check\",\n;; \"arguments\":{\"file_path\":\"lib/std/net/io.ss\"}}\n\n;; A finding includes file, line, symbol, declared arity, and the fixed-prefix threshold.") ("id" . "mcp-chez-reader-and-variadic-ffi-checks") + ";; #!chezscheme library files are verified with the Chez reader path:\n;; {\"name\":\"jerboa_verify\",\n;; \"arguments\":{\"file_path\":\"lib/std/ergo.ss\"}}\n\n;; Lint fixed-arity FFI declarations for known variadic libc symbols:\n;; {\"name\":\"jerboa_variadic_ffi_check\",\n;; \"arguments\":{\"file_path\":\"lib/std/net/io.ss\"}}\n\n;; A finding includes file, line, symbol, declared arity, and the fixed-prefix threshold.") ("id" . "mcp-jerboa-reader-and-variadic-ffi-checks") ("imports") ("notes" . "jerboa_verify and jerboa_compile_check auto-route .sls and #!chezscheme sources through Chez datum reading, which avoids false failures on library-form .ss files with colon-prefixed Chez identifiers. jerboa_variadic_ffi_check complements security_scan by reporting known variadic libc foreign-procedure declarations with arity details and C-shim guidance.") - ("tags" "mcp" "verify" "chez-reader" "ffi" "variadic" + ("tags" "mcp" "verify" "jerboa-reader" "ffi" "variadic" "foreign-procedure") ("title" . @@ -4924,7 +4928,7 @@ "Compile Typed Jerboa scalar modules to LLVM IR and verify/run them")) (("code" . - "(import (jerboa prelude))\n\n;; A mutable counter standing in for any emitter/env state\n(def n 0)\n(def (next!) (set! n (+ n 1)) n)\n(def (peek) n)\n\n;; WRONG: R6RS leaves init order unspecified; Chez runs them RIGHT-TO-LEFT,\n;; so `snapshot` reads the counter BEFORE next! runs.\n(let ([value (next!)]\n [snapshot (peek)])\n (displayln (list value snapshot))) ;; => (1 0) -- snapshot is stale!\n\n;; RIGHT: let* guarantees top-to-bottom evaluation.\n(let* ([value (next!)]\n [snapshot (peek)])\n (displayln (list value snapshot))) ;; => (2 2)") ("id" . "chez-let-init-order-let-star") ("imports") + "(import (jerboa prelude))\n\n;; A mutable counter standing in for any emitter/env state\n(def n 0)\n(def (next!) (set! n (+ n 1)) n)\n(def (peek) n)\n\n;; WRONG: R6RS leaves init order unspecified; Chez runs them RIGHT-TO-LEFT,\n;; so `snapshot` reads the counter BEFORE next! runs.\n(let ([value (next!)]\n [snapshot (peek)])\n (displayln (list value snapshot))) ;; => (1 0) -- snapshot is stale!\n\n;; RIGHT: let* guarantees top-to-bottom evaluation.\n(let* ([value (next!)]\n [snapshot (peek)])\n (displayln (list value snapshot))) ;; => (2 2)") ("id" . "jerboa-let-init-order-let-star") ("imports") ("notes" . "Caused two real bugs in the typed LLVM IR emitter (branch llvmir, 2026-06-03): (1) capturing a basic-block label in the same let as the expression-lowering call that moves it produced malformed phi predecessors on nested if; (2) allocating a fresh SSA register in the same let as operand lowering numbered registers out of order. Both were valid-looking code that only failed on nested control flow. Rule: whenever one init mutates state another init reads (counters, labels, ports, env records), use let*. Add a nested-control-flow test case (if inside else) — flat cases mask the bug. Same hazard applies to plain function-call argument order; bind with let* first when arguments have effects.") @@ -5037,7 +5041,7 @@ "seccomp: default-allow blocklist (deny dangerous syscalls with EPERM)")) (("code" . - ";; WRONG — both (chezscheme) and (std text base64) export base64-encode:\n;; (import (chezscheme) (std text base64))\n;; => Exception: multiple definitions for base64-encode in body\n;;\n;; RIGHT — drop the Chez builtin so the (std ...) version wins:\n(library (my mod)\n (export do-thing)\n (import (except (chezscheme) base64-encode base64-decode)\n (std text base64))\n (def (do-thing bv) (base64-encode bv)))") ("id" . "chez-builtin-shadow-except") + ";; WRONG — both (chezscheme) and (std text base64) export base64-encode:\n;; (import (chezscheme) (std text base64))\n;; => Exception: multiple definitions for base64-encode in body\n;;\n;; RIGHT — drop the Chez builtin so the (std ...) version wins:\n(library (my mod)\n (export do-thing)\n (import (except (chezscheme) base64-encode base64-decode)\n (std text base64))\n (def (do-thing bv) (base64-encode bv)))") ("id" . "jerboa-builtin-shadow-except") ("imports" "(except (chezscheme) base64-encode base64-decode)" "(std text base64)") @@ -5125,13 +5129,13 @@ "Chez does not sequence (let ...) init exprs: use let* when bindings have side effects")) (("code" . - ";; Scheme code can use foreign-procedure names as usual.\n;; In an embedded/standalone binary, the C host must register any symbols that\n;; are resolved from the host process instead of a shared object.\n\n;; example.ss\n(import (jsqlite api))\n(def db (sqlite-open \"/tmp/example.sqlite\"))\n(sqlite-close db)\n\n/* support/main.c excerpt */\n#include \"scheme.h\"\n#include <unistd.h>\n#include <fcntl.h>\n#include <sys/file.h>\n#include <sys/mman.h>\n\nstatic void register_posix_symbols(void) {\n Sforeign_symbol(\"open\", (void *)open);\n Sforeign_symbol(\"close\", (void *)close);\n Sforeign_symbol(\"flock\", (void *)flock);\n Sforeign_symbol(\"usleep\", (void *)usleep);\n Sforeign_symbol(\"ftruncate\", (void *)ftruncate);\n Sforeign_symbol(\"mmap\", (void *)mmap);\n Sforeign_symbol(\"munmap\", (void *)munmap);\n}\n\nint main(int argc, const char *argv[]) {\n Sscheme_init(NULL);\n Sbuild_heap(NULL, NULL);\n register_posix_symbols();\n return Sscheme_program(\"example.ss\", argc, argv);\n}\n") ("id" . "standalone-binary-register-ffi-symbols") + ";; Scheme code can use foreign-procedure names as usual. In an embedded or\n;; standalone static binary, the C host must register symbols that are resolved\n;; from the host process instead of a shared object.\n\n;; example.ss\n(import (jsqlite api))\n(def db (sqlite-open \"/tmp/example.sqlite\"))\n(sqlite-close db)\n\n/* support/main.c excerpt */\n#include \"scheme.h\"\n#include <unistd.h>\n#include <fcntl.h>\n#include <stdio.h>\n#include <time.h>\n#include <sys/file.h>\n#include <sys/mman.h>\n\nstatic void register_posix_symbols(void) {\n Sforeign_symbol(\"open\", (void *)open);\n Sforeign_symbol(\"close\", (void *)close);\n Sforeign_symbol(\"flock\", (void *)flock);\n Sforeign_symbol(\"usleep\", (void *)usleep);\n Sforeign_symbol(\"sleep\", (void *)sleep);\n Sforeign_symbol(\"nanosleep\", (void *)nanosleep);\n Sforeign_symbol(\"ftruncate\", (void *)ftruncate);\n Sforeign_symbol(\"mmap\", (void *)mmap);\n Sforeign_symbol(\"munmap\", (void *)munmap);\n Sforeign_symbol(\"msync\", (void *)msync);\n Sforeign_symbol(\"madvise\", (void *)madvise);\n Sforeign_symbol(\"readlink\", (void *)readlink);\n Sforeign_symbol(\"fdopen\", (void *)fdopen);\n}\n\nint main(int argc, const char *argv[]) {\n Sscheme_init(NULL);\n Sbuild_heap(NULL, NULL);\n register_posix_symbols();\n return Sscheme_program(\"example.ss\", argc, argv);\n}\n") ("id" . "standalone-binary-register-ffi-symbols") ("imports") ("notes" . - "If the embedded program fails with `Exception in foreign-procedure: no entry for \"open\"`, the symbol was visible to dynamically loaded Scheme but not registered in the standalone host. Register libc symbols manually from C with the correct system headers instead of autogenerating declarations such as `extern void open()`, which can conflict with platform prototypes, especially on macOS.") + "If the embedded program fails with `Exception in foreign-procedure: no entry for \"open\"` or another libc name, the symbol was visible to dynamically loaded Scheme but not registered in the standalone host. Register libc symbols manually from C with the correct system headers instead of autogenerating declarations such as `extern void open()`, which can conflict with platform prototypes. Runtime startup can require symbols such as `usleep` through Chez/runtime code even when project Scheme source has no literal `(foreign-procedure \"usleep\" ...)`, so test the produced ELF on the target OS, not just `file` or local generation.") ("tags" "ffi" "standalone-binary" "foreign-procedure" - "Sforeign_symbol" "jsqlite" "jerbuild") + "Sforeign_symbol" "jerbuild" "static") ("title" . "Register foreign-procedure symbols in a standalone binary")) @@ -5245,7 +5249,7 @@ "Serialize Assistant Tool Calls With Empty Content for OpenAI-Compatible Providers")) (("code" . - "(import (jerboa prelude)\n (std text json))\n\n(def h (make-hash-table))\n(hash-put! h \"ok\" #t)\n(displayln (json-object->string h))") ("id" . "plain-chez-repl-module-paths") + "(import (jerboa prelude)\n (std text json))\n\n(def h (make-hash-table))\n(hash-put! h \"ok\" #t)\n(displayln (json-object->string h))") ("id" . "plain-jerboa-repl-module-paths") ("imports" "(jerboa prelude)" "(std text json)") ("notes" . @@ -5306,7 +5310,7 @@ "Track spawned worker liveness with thread-done?")) (("code" . - ";; Pattern for wrapping a C-shaped cdylib API from Jerboa/Chez.\n;; The native side should expose demo_new/demo_free/demo_row_text/demo_last_error.\n\n(define shlib-ext\n (let ([mt (symbol->string (machine-type))])\n (if (and (>= (string-length mt) 3)\n (string=? (substring mt (- (string-length mt) 3) (string-length mt)) \"osx\"))\n \"dylib\"\n \"so\")))\n\n(define static-build?\n (let ([v (getenv \"JEMACS_STATIC\")])\n (and v (not (string=? v \"\")) (not (string=? v \"0\")))))\n\n(define demo-lib-path\n (or (getenv \"DEMO_LIB\")\n (string-append (or (getenv \"DEMO_DIR\") \".\") \"/libdemo.\" shlib-ext)))\n\n(define demo-lib-loaded\n (if static-build? #f (load-shared-object demo-lib-path)))\n\n(define ffi-demo-new (foreign-procedure \"demo_new\" () void*))\n(define ffi-demo-free (foreign-procedure \"demo_free\" (void*) void))\n(define ffi-demo-row-text (foreign-procedure \"demo_row_text\" (void* int u8* size_t) int))\n(define ffi-demo-last-error (foreign-procedure \"demo_last_error\" (u8* size_t) int))\n\n(define (demo-last-error)\n (let* ([buf (make-bytevector 4096 0)]\n [n (ffi-demo-last-error buf 4096)])\n (if (> n 0)\n (utf8->string (let ([out (make-bytevector n)])\n (bytevector-copy! buf 0 out 0 n)\n out))\n \"unknown native error\")))\n\n(define (check-rc who rc)\n (if (< rc 0) (error who (demo-last-error)) rc))\n\n(define (make-demo-session)\n (let ([handle (ffi-demo-new)])\n (if handle (box handle) (error 'make-demo-session (demo-last-error)))))\n\n(define (demo-session-handle! session)\n (let ([handle (unbox session)])\n (if handle handle (error 'demo-session \"native session is closed\"))))\n\n(define (demo-session-free! session)\n (let ([handle (unbox session)])\n (when handle\n (ffi-demo-free handle)\n (set-box! session #f))))\n\n(define (demo-row-text session row)\n (let* ([handle (demo-session-handle! session)]\n [buf (make-bytevector 16384 0)]\n [n (ffi-demo-row-text handle row buf 16384)])\n (check-rc 'demo-row-text n)\n (if (> n 0)\n (utf8->string (let ([out (make-bytevector n)])\n (bytevector-copy! buf 0 out 0 n)\n out))\n \"\")))\n\n;; Use dynamic-wind at call sites when possible so native handles are freed.\n;; (let ([s (make-demo-session)])\n;; (dynamic-wind void (lambda () (demo-row-text s 0))\n;; (lambda () (demo-session-free! s))))") ("id" . "chez-cdylib-opaque-handle-wrapper") + ";; Pattern for wrapping a C-shaped cdylib API from Jerboa/Chez.\n;; The native side should expose demo_new/demo_free/demo_row_text/demo_last_error.\n\n(define shlib-ext\n (let ([mt (symbol->string (machine-type))])\n (if (and (>= (string-length mt) 3)\n (string=? (substring mt (- (string-length mt) 3) (string-length mt)) \"osx\"))\n \"dylib\"\n \"so\")))\n\n(define static-build?\n (let ([v (getenv \"JEMACS_STATIC\")])\n (and v (not (string=? v \"\")) (not (string=? v \"0\")))))\n\n(define demo-lib-path\n (or (getenv \"DEMO_LIB\")\n (string-append (or (getenv \"DEMO_DIR\") \".\") \"/libdemo.\" shlib-ext)))\n\n(define demo-lib-loaded\n (if static-build? #f (load-shared-object demo-lib-path)))\n\n(define ffi-demo-new (foreign-procedure \"demo_new\" () void*))\n(define ffi-demo-free (foreign-procedure \"demo_free\" (void*) void))\n(define ffi-demo-row-text (foreign-procedure \"demo_row_text\" (void* int u8* size_t) int))\n(define ffi-demo-last-error (foreign-procedure \"demo_last_error\" (u8* size_t) int))\n\n(define (demo-last-error)\n (let* ([buf (make-bytevector 4096 0)]\n [n (ffi-demo-last-error buf 4096)])\n (if (> n 0)\n (utf8->string (let ([out (make-bytevector n)])\n (bytevector-copy! buf 0 out 0 n)\n out))\n \"unknown native error\")))\n\n(define (check-rc who rc)\n (if (< rc 0) (error who (demo-last-error)) rc))\n\n(define (make-demo-session)\n (let ([handle (ffi-demo-new)])\n (if handle (box handle) (error 'make-demo-session (demo-last-error)))))\n\n(define (demo-session-handle! session)\n (let ([handle (unbox session)])\n (if handle handle (error 'demo-session \"native session is closed\"))))\n\n(define (demo-session-free! session)\n (let ([handle (unbox session)])\n (when handle\n (ffi-demo-free handle)\n (set-box! session #f))))\n\n(define (demo-row-text session row)\n (let* ([handle (demo-session-handle! session)]\n [buf (make-bytevector 16384 0)]\n [n (ffi-demo-row-text handle row buf 16384)])\n (check-rc 'demo-row-text n)\n (if (> n 0)\n (utf8->string (let ([out (make-bytevector n)])\n (bytevector-copy! buf 0 out 0 n)\n out))\n \"\")))\n\n;; Use dynamic-wind at call sites when possible so native handles are freed.\n;; (let ([s (make-demo-session)])\n;; (dynamic-wind void (lambda () (demo-row-text s 0))\n;; (lambda () (demo-session-free! s))))") ("id" . "jerboa-cdylib-opaque-handle-wrapper") ("imports" "(chezscheme)") ("notes" . @@ -5447,4 +5451,508 @@ "Names such as rows can be parsed as SQL keywords by jsqlite. Quote keyword-like identifiers consistently in CREATE TABLE, INSERT, UPDATE, and SELECT statements. This is preferable to relying on a parser accepting bare identifiers that happen to work in another SQLite implementation.") ("tags" "jsqlite" "sqlite" "sql" "identifier" "keyword" "schema") - ("title" . "Quote SQL keyword column names in jsqlite"))) + ("title" . "Quote SQL keyword column names in jsqlite")) + (("code" + . + "# In a wrapper script invoked by the top-level Makefile.\n# Resolve JERBUILD before calling make -C so relative paths stay rooted at\n# the top-level build directory instead of the vendored repo directory.\nif [ -n \"${JERBUILD:-}\" ]; then\n case \"${JERBUILD}\" in\n /*) ;;\n */*) JERBUILD=\"$(cd \"$(dirname \"${JERBUILD}\")\" && pwd)/$(basename \"${JERBUILD}\")\" ;;\n esac\n export JERBUILD\nfi\n\nif [ -n \"${JERBUILD:-}\" ]; then\n \"${MAKE:-make}\" -C \"${repo}\" \"JERBUILD=${JERBUILD}\" \"${target}\"\nelse\n \"${MAKE:-make}\" -C \"${repo}\" \"${target}\"\nfi\n") ("id" . "jerbuild-submake-absolute-path") ("imports") + ("notes" + . + "Top-level Jerboa Makefiles often select a local bundled tool with a relative path such as ../jerboa/dist/jerbuild. A child make run with make -C vendor/repo evaluates that relative path from the vendored repo directory, causing 'jerbuild not found' even though the parent build resolved it correctly. Resolve JERBUILD to an absolute path before delegation and pass it as an explicit make variable.") + ("tags" "jerbuild" "makefile" "submake" "vendor" + "cross-build" "path") + ("title" + . + "Pass an absolute jerbuild path into make -C sub-builds")) + (("code" + . + "(import (jerboa prelude))\n(import (std os exec-id))\n(import (std os limits sandbox))\n(import (std os supervise))\n(import (std os path))\n\n(def (path-with-dir p)\n (if (and p (string? p))\n (list p (path-directory p))\n '()))\n\n(def id (exec-id-resolve \"sh\"))\n(def argv (list (exec-id-path id) \"-c\" \"printf ok\"))\n(def support (append (path-with-dir (exec-id-path id))\n (path-with-dir (exec-id-realpath id))))\n(def env `((\"PATH\" . ,(string-join support \":\"))\n (\"HOME\" . ,(or (getenv \"HOME\") \"/\"))))\n(def policy\n (sandbox-policy\n read-paths: support\n exec-paths: support\n write-paths: (list \"/tmp\" \"/private/tmp\")\n net: 'allow\n syscalls: 'safe\n capsicum?: #f))\n(def result\n (sandbox-launch policy\n command: argv\n env: env\n cwd: (current-directory)\n capture-stdout?: #t\n capture-stderr?: #t\n require: (if (equal? (machine-type) 'tarm64osx) '(fs) '())\n fail-closed?: #t))\n(def proc (sandbox-result-process result))\n(display (utf8->string (process-result-stdout proc)))\n(newline)") ("id" . "external-cli-sandbox-exec-id-env") + ("imports" "(jerboa prelude)" "(std os exec-id)" + "(std os limits sandbox)" "(std os supervise)" + "(std os path)") + ("notes" + . + "Resolve the executable before sandbox launch because macOS sandbox-exec may run after Jerboa's normal PATH search. Use exec-id-path as argv[0] to preserve symlink/wrapper semantics; include exec-id-realpath and both directories in read/exec support paths so the sandbox does not block the target. Do not pass env: #f for shebang CLIs such as /usr/bin/env node; provide a minimal PATH/HOME environment. Avoid forwarding the parent PATH wholesale because it can include provider auth/temp directories that the sandbox intentionally denies.") + ("tags" "sandbox" "exec-id" "sandbox-exec" "external-cli" + "environment" "macos") + ("title" + . + "Launch external CLIs through sandbox with exec-id and minimal env")) + (("code" + . + "(import (std text json))\n\n(define (assoc-args? args)\n (and (pair? args) (pair? (car args))))\n\n(define (assoc-args->hash args)\n (let ((ht (make-hash-table)))\n (for-each (lambda (kv) (hash-put! ht (car kv) (cdr kv))) args)\n ht))\n\n(define (tool-call-arguments->json-string args)\n (cond\n ((string? args) args) ; already encoded\n ((hash-table? args) (json-object->string args))\n ((null? args) \"{}\") ; no-arg tool call: OpenAI requires object, not []\n ((assoc-args? args) (json-object->string (assoc-args->hash args)))\n (else (json-object->string args))))") ("id" . "openai-tool-call-args-empty-object") + ("imports" "(std text json)") + ("notes" + . + "OpenAI-compatible providers reject tool_call.function.arguments when it is a JSON array. Workflow/proxy adapters often parse an empty JSON object into an empty assoc/list '(), and naively serializing that produces [] instead of {}. Put the null? branch before generic list/JSON serialization. Non-empty assoc args should be converted to a hash table before json-object->string.") + ("tags" "openai-compatible" "tool-calls" "json" + "message->json" "mlx" "workflow") + ("title" + . + "Serialize no-arg OpenAI tool calls as an empty JSON object")) + (("code" + . + "(import (jerboa prelude))\n(import (only (std os shell) shell-quote))\n(import (only (std security taint) safe-system))\n\n(def (run-cargo-build crate-dir)\n (let* ([cmd (format \"cd ~a && cargo build --release --no-default-features --features tls,crypto 2>&1\"\n (shell-quote crate-dir))]\n [rc (safe-system cmd)])\n (unless (= rc 0)\n (error 'run-cargo-build \"cargo build failed\" crate-dir))\n rc))\n\n(def (archive-has-sqlite-symbols? archive)\n (= 0 (safe-system\n (format \"command -v nm >/dev/null 2>&1 && nm -g ~a 2>/dev/null | grep -E 'jerboa_sqlite_|sqlite3_' >/dev/null\"\n (shell-quote archive)))))") ("id" . "safe-system-shell-quoted-paths") + ("imports" + "(jerboa prelude)" + "(std os shell)" + "(std security taint)") + ("notes" + . + "Use shell-quote for each filesystem path or other command token that is interpolated into a shell command. safe-system is still useful because the security scanner recognizes it as the checked sink, but it does not replace shell quoting. The command string should keep fixed shell syntax literal and only interpolate quoted values.") + ("tags" "system" "shell-quote" "safe-system" "taint" + "build-script" "path") + ("title" + . + "Run shell commands with quoted paths and safe-system")) + (("code" + . + "(import (jerboa prelude))\n\n;; Prompt for sensitive input before a TUI library takes over the terminal.\n;; This avoids hidden input getting swallowed by raw-mode/event-loop handling.\n(def (prompt-hidden-line prompt)\n (display prompt)\n (flush-output-port (current-output-port))\n (dynamic-wind\n (lambda ()\n (guard (e [#t (void)])\n (system \"stty -echo 2>/dev/null\")))\n (lambda ()\n (let ([line (get-line (current-input-port))])\n (newline)\n (if (eof-object? line) \"\" line)))\n (lambda ()\n (guard (e [#t (void)])\n (system \"stty echo 2>/dev/null\")))))\n\n;; Minimal stand-ins for app code; replace these with log DB open/close and\n;; termbox/curses init in a real TUI.\n(def (open-sensitive-resource passphrase)\n (string-append \"opened-with-\" (number->string (string-length passphrase)) \"-chars\"))\n\n(def (close-sensitive-resource resource)\n (display \"closed resource\")\n (newline))\n\n(def (with-terminal-ui thunk)\n ;; Real apps initialize raw mode here, after the prompt is complete.\n (thunk))\n\n(def (run-event-loop resource)\n (display \"TUI can start now; resource = \")\n (display resource)\n (newline))\n\n(def (run-app)\n ;; Open password-protected resources before terminal init.\n (let ([passphrase (prompt-hidden-line \"passphrase: \")]\n [resource #f])\n (dynamic-wind\n (lambda () (void))\n (lambda ()\n (set! resource (and (not (string=? passphrase \"\"))\n (open-sensitive-resource passphrase)))\n ;; Start subprocesses and initialize the TUI only after prompting.\n (with-terminal-ui\n (lambda () (run-event-loop resource))))\n (lambda ()\n (when resource\n (close-sensitive-resource resource))))))\n\n(run-app)") ("id" . "prompt-before-terminal-raw-mode") + ("imports" "(jerboa prelude)") + ("notes" + . + "Useful for termbox/curses-style TUIs and any app that needs getpass-like input. Prompting after raw-mode init can appear to hang or can consume input invisibly. Starting slow subprocesses before the prompt can also hide the real wait behind an absent password prompt. Use dynamic-wind so echo is restored and opened resources are closed on errors.") + ("tags" "tui" "terminal" "passphrase" "dynamic-wind" + "termbox" "raw-mode") + ("title" . "Prompt Before Entering Terminal Raw Mode")) + (("code" + . + "(import (jerboa prelude))\n\n(def raw-args (command-line-arguments))\n(def args\n (if (and (pair? raw-args)\n (string? (car raw-args))\n (string-suffix? \".ss\" (car raw-args)))\n (cdr raw-args)\n raw-args))\n\n(def (usage)\n (display \"usage: sum.ss A B\")\n (newline))\n\n(def (parse-number label value)\n (let ([n (string->number value)])\n (if n n (error 'script \"expected numeric argument\" label value))))\n\n(if (= (length args) 2)\n (let ([a (parse-number \"A\" (list-ref args 0))]\n [b (parse-number \"B\" (list-ref args 1))])\n (display (+ a b))\n (newline))\n (usage))") ("id" . "jerboa-cli-script-args-verify-friendly") + ("imports") + ("notes" + . + "Run with `/Users/user/mine/jerboa/.chez/bin/scheme --libdirs /Users/user/mine/jerboa/lib --script sum.ss 7 5`. `scheme --script` gives user args directly, while some wrappers may include the script path; the normalizer supports both. Keep the no-arg path non-throwing so `jerboa_verify`/compile checks do not fail before the smoke run with real args.") + ("tags" "script" "cli" "command-line-arguments" "verify" + "scheme-script" "jerboa") + ("title" + . + "CLI scripts with command-line-arguments that pass verification")) + (("code" + . + "(import (jerboa prelude))\n\n(def (skip-leading-spaces s start len)\n (let loop ((i start))\n (if (and (< i len) (char-whitespace? (string-ref s i)))\n (loop (+ i 1))\n i)))\n\n(def (find-wrap-break s start end)\n (let loop ((i (- end 1)))\n (cond\n ((<= i start) -1)\n ((char-whitespace? (string-ref s i)) i)\n (else (loop (- i 1))))))\n\n(def (wrap-text s width)\n (cond\n ((<= width 0) '(\"\"))\n (#t\n (let ((len (string-length s)))\n (let loop ((start (skip-leading-spaces s 0 len)) (acc '()))\n (cond\n ((>= start len)\n (if (null? acc) '(\"\") (reverse acc)))\n (#t\n (let ((end (min len (+ start width))))\n (cond\n ((>= end len)\n (reverse (cons (substring s start len) acc)))\n (#t\n (let ((cut-point (find-wrap-break s start end)))\n (if (>= cut-point 0)\n (loop (skip-leading-spaces s (+ cut-point 1) len)\n (cons (substring s start cut-point) acc))\n (loop (skip-leading-spaces s end len)\n (cons (substring s start end) acc))))))))))))))\n\n(assert! (equal? (wrap-text \"Alpha beta gamma delta\" 12)\n '(\"Alpha beta\" \"gamma delta\")))\n(assert! (equal? (wrap-text \"abcdefghijk\" 4)\n '(\"abcd\" \"efgh\" \"ijk\")))\n(displayln \"ok\")") ("id" . "word-wrap-string-width") + ("imports" "(jerboa prelude)") + ("notes" + . + "Splits on whitespace when a break point exists inside the width, otherwise hard-wraps long tokens. Leading spaces on continuation chunks are skipped. The example uses -1 as a sentinel for \"no break\" so all helper return values are integers.") + ("tags" "string" "wrap" "substring" "char-whitespace" + "terminal" "tui") + ("title" . "Wrap a String to a Fixed Character Width")) + (("code" + . + "(import (jerboa prelude))\n(import (chezscheme))\n(import (jerboa-qt qt))\n\n(def immediate-exit (foreign-procedure \"_exit\" (int) void))\n\n(def (finish status)\n (flush-output-port (current-output-port))\n (flush-output-port (current-error-port))\n (immediate-exit status))\n\n(def (pump! app n)\n (when (> n 0)\n (qt-app-process-events! app)\n (pump! app (- n 1))))\n\n(def app (qt-app-create))\n(def win (qt-main-window-create))\n(def canvas (qt-paint-widget-create))\n(def paint-count (vector 0))\n(def key-code (vector #f))\n\n(qt-widget-set-minimum-size! canvas 120 160)\n(qt-on-paint! canvas\n (lambda ()\n (vector-set! paint-count 0 (+ (vector-ref paint-count 0) 1))\n (let ((p (qt-paint-widget-painter canvas)))\n (when p\n (qt-painter-fill-rect! p 0 0 120 160 24 26 32 255)\n (qt-painter-fill-rect! p 20 20 30 30 239 83 80 255)))))\n(qt-main-window-set-central-widget! win canvas)\n(qt-on-key-press! win\n (lambda ()\n (vector-set! key-code 0 (qt-last-key-code))))\n(qt-widget-show! win)\n(qt-widget-set-focus! win)\n(qt-paint-widget-update! canvas)\n(pump! app 8)\n(unless (> (vector-ref paint-count 0) 0)\n (error 'smoke \"paint callback did not run\"))\n(unless (qt-widget-screenshot! win \"/tmp/jerboa-qt-smoke.png\")\n (error 'smoke \"screenshot failed\"))\n(qt-send-key-press! win QT_KEY_LEFT QT_MOD_NONE \"\")\n(pump! app 4)\n(unless (equal? (vector-ref key-code 0) QT_KEY_LEFT)\n (error 'smoke \"synthetic key press was not observed\"))\n(displayln \"qt smoke passed\")\n(finish 0)\n") ("id" . "jerboa-qt-offscreen-screenshot-smoke") + ("imports" + "(jerboa prelude)" + "(chezscheme)" + "(jerboa-qt qt)") + ("notes" + . + "Run with QT_QPA_PLATFORM=offscreen and the jerboa-qt shim variables set, for example through `make ladder-smoke` in jerboa-qt. Pump events before screenshots and after synthetic key events. The `_exit` call is harness-only: this Qt shim can crash during normal Scheme/C++ teardown after tests have already reported, so smoke tests flush output and terminate immediately. Do not use `_exit` in normal apps.") + ("tags" "qt" "offscreen" "screenshot" "paint-widget" + "keyboard" "jerboa-qt") + ("title" + . + "Jerboa Qt offscreen screenshot and key-input smoke test")) + (("code" + . + "(import (jerboa prelude))\n(import (chezscheme))\n\n;; Unsafe pattern in a C shim:\n;; extern \"C\" const char* item_text(...) {\n;; if (!item) return nullptr; // BAD for Chez `string` result\n;; return strdup(item->text().toUtf8().constData());\n;; }\n;;\n;; Jerboa/Chez binding:\n;; (foreign-procedure \"item_text\" (void* int) string)\n;;\n;; Correct C shim shape:\n;; extern \"C\" const char* item_text(...) {\n;; if (!item) return \"\"; // Safe empty string sentinel\n;; return stable_string_copy(...);\n;; }\n;;\n;; Test-side workaround if the shim is external and cannot be changed:\n;; avoid calling the string-returning FFI function for cases known to return\n;; NULL, and assert only counts/state until the shim returns a non-NULL string.\n(displayln \"Use non-NULL C strings for Chez FFI string returns.\")\n") ("id" . "chez-ffi-null-c-string-gotcha") + ("imports" "(jerboa prelude)" "(chezscheme)") + ("notes" + . + "Chez `foreign-procedure` with return type `string` expects a valid non-NULL `char*`. Returning NULL from C/C++ can segfault before Scheme code can handle it. This came up with `qt-table-widget-item-text` on an empty Qt cell. Prefer fixing the C shim to return an empty string or a separate status/value API; if the shim is external, gate or skip that assertion in tests.") + ("tags" "ffi" "chezscheme" "string" "null" "c-shim" "qt") + ("title" + . + "Chez FFI string return values cannot be NULL C strings")) + (("code" + . + "#!/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.") + ("tags" "jerboa-qt" "jerbuild" "wrapper" "ffi" "dyld" + "example") + ("title" + . + "Run a jerboa-qt example with jerbuild and shim environment")) + (("code" + . + "(import (jerboa prelude))\n(import (chezscheme))\n(import (jerboa-qt qt))\n\n(def immediate-exit (foreign-procedure \"_exit\" (int) void))\n\n(def (finish status)\n (flush-output-port (current-output-port))\n (flush-output-port (current-error-port))\n (immediate-exit status))\n\n(def (pump! app n)\n (when (> n 0)\n (qt-app-process-events! app)\n (pump! app (- n 1))))\n\n(def (draw-cell! painter col row r g b)\n (qt-painter-fill-rect! painter\n (+ 12 (* col 18))\n (+ 12 (* row 18))\n 16 16 r g b 255))\n\n(def (draw-board! painter)\n (qt-painter-fill-rect! painter 0 0 220 400 24 26 32 255)\n (let row-loop ((row 0))\n (when (< row 20)\n (let col-loop ((col 0))\n (when (< col 10)\n (draw-cell! painter col row 44 48 58)\n (col-loop (+ col 1))))\n (row-loop (+ row 1)))))\n\n(def app (qt-app-create))\n(def win (qt-main-window-create))\n(def canvas (qt-paint-widget-create))\n(def pos (vector 4 3))\n(def paint-count (vector 0))\n\n(qt-widget-set-minimum-size! canvas 220 400)\n(qt-on-paint! canvas\n (lambda ()\n (vector-set! paint-count 0 (+ (vector-ref paint-count 0) 1))\n (let ((p (qt-paint-widget-painter canvas)))\n (when p\n (draw-board! p)\n (let ((col (vector-ref pos 0))\n (row (vector-ref pos 1)))\n (draw-cell! p col row 132 204 22)\n (draw-cell! p (+ col 1) row 132 204 22)\n (draw-cell! p col (+ row 1) 132 204 22)\n (draw-cell! p (+ col 1) (+ row 1) 132 204 22))))))\n\n(qt-on-key-press! win\n (lambda ()\n (let ((key (qt-last-key-code))\n (col (vector-ref pos 0)))\n (cond\n ((= key QT_KEY_LEFT)\n (vector-set! pos 0 (max 0 (- col 1))))\n ((= key QT_KEY_RIGHT)\n (vector-set! pos 0 (min 8 (+ col 1))))))\n (qt-paint-widget-update! canvas)))\n\n(qt-main-window-set-title! win \"Stateful Board\")\n(qt-main-window-set-central-widget! win canvas)\n(qt-widget-resize! win 240 460)\n(qt-widget-show! win)\n(qt-widget-set-focus! win)\n(qt-paint-widget-update! canvas)\n(pump! app 8)\n\n(qt-send-key-press! win QT_KEY_RIGHT QT_MOD_NONE \"\")\n(pump! app 4)\n(unless (= (vector-ref pos 0) 5)\n (error 'stateful-board \"right key did not move piece\"))\n\n(qt-send-key-press! win QT_KEY_LEFT QT_MOD_NONE \"\")\n(qt-send-key-press! win QT_KEY_LEFT QT_MOD_NONE \"\")\n(pump! app 4)\n(unless (= (vector-ref pos 0) 3)\n (error 'stateful-board \"left keys did not move piece\"))\n\n(unless (> (vector-ref paint-count 0) 0)\n (error 'stateful-board \"paint callback did not run\"))\n(unless (qt-widget-screenshot! win \"/tmp/jerboa-qt-stateful-board.png\")\n (error 'stateful-board \"screenshot failed\"))\n\n(displayln \"stateful board passed\")\n(finish 0)\n") ("id" . "jerboa-qt-stateful-board-key-redraw") + ("imports" + "(jerboa prelude)" + "(chezscheme)" + "(jerboa-qt qt)") + ("notes" + . + "Use a mutable vector for small board/piece state, mutate it in a `qt-on-key-press!` handler, and call `qt-paint-widget-update!` after state changes so the paint callback redraws from authoritative state. Clamp horizontal movement to the board width minus the active piece width. Run with `QT_QPA_PLATFORM=offscreen` and the jerboa-qt shim environment set. `_exit` is only for smoke-test harnesses that must avoid Qt/C++ teardown crashes; do not use it in normal apps.") + ("tags" "jerboa-qt" "qt" "paint-widget" "keyboard" + "stateful" "redraw") + ("title" + . + "Jerboa Qt stateful board redraw from keyboard input")) + (("code" + . + "(import (jerboa prelude))\n(import (chezscheme))\n(import (jerboa-qt qt))\n\n(def immediate-exit (foreign-procedure \"_exit\" (int) void))\n\n(def (finish status)\n (flush-output-port (current-output-port))\n (flush-output-port (current-error-port))\n (immediate-exit status))\n\n(def (pump! app n)\n (when (> n 0)\n (qt-app-process-events! app)\n (pump! app (- n 1))))\n\n(def (pump-wait! app n delay-ms)\n (when (> n 0)\n (sleep-ms delay-ms)\n (qt-app-process-events! app)\n (pump-wait! app (- n 1) delay-ms)))\n\n(def app (qt-app-create))\n(def win (qt-main-window-create))\n(def canvas (qt-paint-widget-create))\n(def timer (qt-timer-create))\n(def piece-row (vector 0))\n(def paint-count (vector 0))\n\n(qt-widget-set-minimum-size! canvas 220 400)\n(qt-on-paint! canvas\n (lambda ()\n (vector-set! paint-count 0 (+ (vector-ref paint-count 0) 1))\n (let ((p (qt-paint-widget-painter canvas)))\n (when p\n (qt-painter-fill-rect! p 0 0 220 400 24 26 32 255)\n (qt-painter-fill-rect! p 84 (+ 12 (* (vector-ref piece-row 0) 18))\n 34 34 132 204 22 255)))))\n\n(qt-on-timeout! timer\n (lambda ()\n (vector-set! piece-row 0 (min 18 (+ (vector-ref piece-row 0) 1)))\n (qt-paint-widget-update! canvas)))\n\n(qt-main-window-set-title! win \"Timer Redraw\")\n(qt-main-window-set-central-widget! win canvas)\n(qt-widget-resize! win 240 460)\n(qt-widget-show! win)\n(qt-paint-widget-update! canvas)\n(pump! app 4)\n\n(qt-timer-start! timer 20)\n(pump-wait! app 10 25)\n(qt-timer-stop! timer)\n\n(unless (> (vector-ref piece-row 0) 0)\n (error 'timer-redraw \"timer did not advance state\"))\n(unless (> (vector-ref paint-count 0) 0)\n (error 'timer-redraw \"paint callback did not run\"))\n\n(let ((stopped-row (vector-ref piece-row 0)))\n (pump-wait! app 4 25)\n (unless (= (vector-ref piece-row 0) stopped-row)\n (error 'timer-redraw \"timer kept firing after stop\")))\n\n(unless (qt-widget-screenshot! win \"/tmp/jerboa-qt-timer-redraw.png\")\n (error 'timer-redraw \"screenshot failed\"))\n\n(qt-timer-destroy! timer)\n(displayln \"timer redraw passed\")\n(finish 0)\n") ("id" . "jerboa-qt-offscreen-timer-redraw") + ("imports" + "(jerboa prelude)" + "(chezscheme)" + "(jerboa-qt qt)") + ("notes" + . + "For offscreen timer tests, start the timer with `(qt-timer-start! timer msec)`, then alternate `sleep-ms` with `qt-app-process-events!` so wall-clock timer events can fire. Timeout handlers should mutate authoritative state and then call `qt-paint-widget-update!`. Stop the timer before asserting it no longer moves. `_exit` is harness-only for Qt smoke tests that must avoid C++ teardown crashes; normal apps should use the Qt event loop normally.") + ("tags" "jerboa-qt" "qt" "timer" "offscreen" "paint-widget" + "sleep-ms") + ("title" + . + "Jerboa Qt offscreen timer-driven redraw smoke test")) + (("code" + . + "(import (jerboa prelude))\n\n(def (string-prefix-local? prefix s)\n (let ([plen (string-length prefix)]\n [slen (string-length s)])\n (and (>= slen plen)\n (string=? (substring s 0 plen) prefix))))\n\n(def (path-basename path)\n (let ([len (string-length path)])\n (let loop ([i (- len 1)])\n (cond\n [(< i 0) path]\n [(char=? (string-ref path i) #\\/)\n (substring path (+ i 1) len)]\n [else (loop (- i 1))]))))\n\n(def (jsh-shell-path? self-path path)\n (and path\n (> (string-length path) 0)\n (let ([base (path-basename path)])\n (or (string=? path self-path)\n (string=? base \"jsh\")\n (string-prefix-local? \"jsh-\" base)\n (string-prefix-local? \".jsh-program-\" base)))))\n\n(def (valid-pane-shell self-path path)\n (and path\n (> (string-length path) 0)\n (not (jsh-shell-path? self-path path))\n path))\n\n(def (pane-shell-path self-path mux-shell shell)\n (or (valid-pane-shell self-path mux-shell)\n (valid-pane-shell self-path shell)\n (if (file-exists? \"/bin/bash\") \"/bin/bash\" \"/bin/sh\")))\n\n(displayln (pane-shell-path \"/usr/local/bin/jsh\" #f \"/tmp/.jsh-program-1234.so\"))") ("id" . "mux-static-binary-pane-shell-fallback") + ("imports" "(jerboa prelude)") + ("notes" + . + "In embedded/static jsh builds, the interactive shell may set SHELL to an extracted temporary program such as /tmp/.jsh-program-1234.so. If a mux pane blindly execs $SHELL -i, the child exits 126 and the server can repeatedly respawn blank panes. Treat self paths, jsh-looking basenames, and .jsh-program-* basenames as invalid pane shells; allow an explicit non-jsh override such as JSH_MUX_SHELL before falling back to a real system shell.") + ("tags" "mux" "static-binary" "SHELL" "pty" + "embedded-program" "shell-fallback") + ("title" + . + "Avoid recursive mux panes from static-binary SHELL values")) + (("code" + . + ";;; Contract verifier file. Generated module must export:\n;;; (make-static-board-window paint-count-box) -> values window canvas\n(import (jerboa prelude))\n(import (chezscheme))\n(import (jerboa-qt qt))\n(import (huihui static-board))\n\n(def immediate-exit (foreign-procedure \"_exit\" (int) void))\n\n(def (finish status)\n (flush-output-port (current-output-port))\n (flush-output-port (current-error-port))\n (immediate-exit status))\n\n(def (check expr message)\n (unless expr\n (error 'generated-qt-contract \"~a\" message)))\n\n(def (pump! app n)\n (when (> n 0)\n (qt-app-process-events! app)\n (pump! app (- n 1))))\n\n(def app (qt-app-create))\n(def paint-count-box (vector 0))\n\n(let-values (((win canvas) (make-static-board-window paint-count-box)))\n (check (not (eqv? win 0)) \"window handle should be nonzero\")\n (check (not (eqv? canvas 0)) \"canvas handle should be nonzero\")\n (qt-widget-show! win)\n (qt-paint-widget-update! canvas)\n (pump! app 10)\n (check (> (vector-ref paint-count-box 0) 0)\n \"paint callback did not increment verifier-owned counter\")\n (check (qt-widget-screenshot! win \"/tmp/generated-qt-contract.png\")\n \"screenshot failed\")\n (displayln \"generated Qt contract passed\")\n (finish 0))\n") ("id" . "jerboa-qt-generated-module-contract") + ("imports" + "(jerboa prelude)" + "(chezscheme)" + "(jerboa-qt qt)" + "(huihui static-board)") + ("notes" + . + "Use this when an AI/model should generate a Qt module but the verifier must own QApplication lifecycle, event pumping, screenshots, and assertions. Require the generated module to return handles and increment a verifier-owned `paint-count-box` from its paint callback; this proves the callback actually ran without requiring pixel inspection. Keep `_exit` in the verifier only, not generated app code, because it is a harness workaround for Qt/C++ teardown crashes. The generated module should not call `qt-app-create`, `qt-app-exec!`, `qt-widget-show!`, or `_exit`; the verifier controls those.") + ("tags" "jerboa-qt" "contract" "generated-module" + "offscreen" "paint-widget" "verification") + ("title" + . + "Verifier-owned contract for generated Jerboa Qt modules")) + (("code" + . + ";;; Verifier-owned examples for checking generated Tetris state, controls, and rendering.\n(import (jerboa prelude))\n(import (chezscheme))\n(import (jerboa-qt qt))\n(import (huihui tetris-game))\n\n(def (send-key! app win key)\n (qt-send-key-press! win key QT_MOD_NONE \"\")\n (pump! app 4))\n\n(def (same-cells? a b)\n (equal? (sort cell<? a)\n (sort cell<? b)))\n\n(def (check-piece-rotates! app win state piece)\n (clear-board! state)\n (debug-force-active! state piece 3 0)\n (let ((before-rotation (debug-active-cells state)))\n (send-key! app win QT_KEY_UP)\n (let ((after-rotation (debug-active-cells state)))\n (check (not (same-cells? before-rotation after-rotation))\n (str \"up key should rotate active \" piece \" piece\")))))\n\n;; Every non-square tetromino family should rotate, not only I.\n(for-each (lambda (piece)\n (check-piece-rotates! app win state piece))\n '(I T S Z J L))\n\n;; Rotation legality needs targeted wall/collision cases, not only a happy-path rotate.\n(clear-board! state)\n(debug-force-active! state 'I 3 17)\n(let ((before-rotation (debug-active-cells state)))\n (send-key! app win QT_KEY_UP)\n (check (same-cells? (debug-active-cells state) before-rotation)\n \"up key should not rotate active piece through the bottom wall\"))\n\n;; Timer gravity should lock when blocked, not only move when space is available.\n(qt-timer-set-single-shot! timer #t)\n(clear-board! state)\n(debug-board-set! state 4 19 'X)\n(debug-force-active! state 'O 4 17)\n(qt-timer-start! timer 20)\n(pump-wait! app 5 25)\n(check (debug-board-ref state 4 17)\n \"timer should lock active piece when downward movement is blocked\")\n(qt-timer-set-single-shot! timer #f)\n\n;; Soft drop must also be blocked, not just move on an empty board.\n(clear-board! state)\n(debug-force-active! state 'O 4 18)\n(send-key! app win QT_KEY_DOWN)\n(check (= (debug-active-row state) 18)\n \"down key should not move active piece through the bottom wall\")\n\n;; Hard drop should lock above occupied cells, then spawn the next piece.\n(clear-board! state)\n(debug-board-set! state 4 5 'X)\n(debug-force-active! state 'O 4 0)\n(send-key! app win QT_KEY_SPACE)\n(check (debug-board-ref state 4 3)\n \"space key should hard-drop and lock active piece above occupied cells\")\n(check (= (debug-active-row state) 0)\n \"space key should spawn next active piece after blocked hard drop\")\n\n;; A single lock can clear multiple rows; record baselines because earlier probes may update score/lines.\n(clear-board! state)\n(fill-row-except! state 18 4 5)\n(fill-row-except! state 19 4 5)\n(debug-force-active! state 'O 4 16)\n(let ((lines-before (vector-ref lines-box 0))\n (score-before (vector-ref score-box 0)))\n (debug-step! state)\n (debug-step! state)\n (debug-step! state)\n (check (>= (- (vector-ref lines-box 0) lines-before) 2)\n \"double line clear should increase line count by at least two\")\n (check (> (vector-ref score-box 0) score-before)\n \"double line clear should increase score\"))\n\n;; Repeated hard drops should vary spawned footprints, preferably through a deterministic cycle.\n(let ((spawn-footprints (collect-hard-drop-spawn-footprints! app win state 6)))\n (check (> (length (unique spawn-footprints)) 1)\n \"hard-drop spawn sequence should produce more than one tetromino footprint\"))\n\n;; Before screenshot, force a render state with known pixel coordinates.\n(clear-board! state)\n(debug-board-set! state 7 10 'X)\n(debug-force-active! state 'O 2 3)\n(qt-paint-widget-update! canvas)\n(pump! app 6)\n(check (qt-widget-screenshot! win \"/tmp/jerboa-qt-huihui-tetris-game.png\")\n \"tetris game screenshot failed\")\n\n;; Shell-side follow-up:\n;; scripts/huihui-verify-snapshots pixel \\\n;; /tmp/jerboa-qt-huihui-tetris-game.png 19 19 32 42 56 \"empty cell\" \\\n;; /tmp/jerboa-qt-huihui-tetris-game.png 59 79 64 180 120 \"active piece\" \\\n;; /tmp/jerboa-qt-huihui-tetris-game.png 159 219 190 82 80 \"locked cell\"\n") ("id" . "jerboa-qt-game-debug-contract") + ("imports" + "(jerboa prelude)" + "(chezscheme)" + "(jerboa-qt qt)" + "(huihui tetris-game)") + ("notes" + . + "For generated interactive games, keep the verifier in charge of QApplication, event pumping, synthetic keys, timer start/stop, and screenshots. Require generated code to expose a small debug surface such as `debug-force-active!`, `debug-active-col`, `debug-active-row`, `debug-active-cells`, `debug-board-ref`, `debug-board-set!`, and `debug-step!`. Use public key events plus debug hooks to verify legality: left/right, rotation, and soft drop must not cross board walls or move into occupied cells. Rotation should be checked for every non-square family (`I`, `T`, `S`, `Z`, `J`, and `L`), because checking only `I` allows an I-only rotation implementation to pass. Timer callbacks must exercise the same gravity/lock path as `debug-step!`; use `qt-timer-set-single-shot!` in the verifier for deterministic blocked-timer probes, then restore normal timer mode. For hard drop, add a negative fixture that respects board bounds but ignores occupied cells; this avoids failing earlier bottom/spawn checks and proves the intended blocker case. Include a two-row completion from one active-piece lock and compare `lines-box`/`score-box` against baselines. For screenshots, force a deterministic debug state immediately before `qt-widget-screenshot!` and then run exact RGB pixel checks out-of-process. This catches stale/decorative renderers that pass behavioral state checks and distinct-color screenshot checks but do not paint current active/locked cells. `_exit` and direct `(chezscheme)` imports are harness-only and should not be copied into generated app code; generated-module static audits should forbid them.") + ("tags" "jerboa-qt" "game" "debug-hooks" "contract" + "generated-module" "verification" "tetris" "pixel" + "screenshot" "collision") + ("title" + . + "Verifier debug hooks for generated Jerboa Qt games")) + (("code" + . + "#!/usr/bin/env sh\nset -eu\n\n# In a generated-code ladder, keep contract tests fixed in the repo, then prove\n# they can go green by creating temporary reference modules outside the repo.\nrepo_root=$(pwd)\ntmpdir=$(mktemp -d \"${TMPDIR:-/tmp}/contract-reference.XXXXXX\")\ntrap 'rm -rf \"$tmpdir\"' EXIT INT TERM\nmkdir -p \"$tmpdir/src/example\" \"$tmpdir/lib\"\n\ncat > \"$tmpdir/src/example/generated.ss\" <<'EOF'\n(export answer)\n(import (jerboa prelude))\n(def answer 42)\nEOF\n\njerbuild transpile \"$tmpdir/src\" \"$tmpdir/lib\" --force\nJH=$(jerbuild --jerboa-home)\njerbuild exec --libdirs \"$tmpdir/lib:$repo_root/lib:$JH/lib\" tests/generated-contract.ss\n") ("id" . "jerboa-generated-contract-reference-self-test") + ("imports" "(jerboa prelude)") + ("notes" + . + "Use this when AI-generated modules are expected later but the verifier is owned by the ladder. Keep the real repo source absent for expected-red checks, then create temporary reference .ss modules under /tmp and prepend their transpiled libdir when running the fixed contract files. For Qt contracts, pass the same FFI environment variables used by normal offscreen tests. This proves failures during model runs are attributable to generated code, not a broken verifier.") + ("tags" "jerboa" "contract" "generated-module" + "reference-module" "jerbuild" "verification") + ("title" + . + "Temporary reference modules for generated-code contract self-tests")) + (("code" + . + "#!/usr/bin/env sh\n# After a Qt offscreen smoke or contract test writes screenshots, decode PNGs and\n# assert dimensions, distinct-color count, and optional exact RGB pixels.\n\nscripts/huihui-verify-snapshots spec \\\n /tmp/jerboa-qt-huihui-tetris-game.png 240 460 3 1000 \"huihui tetris game\"\n\nscripts/huihui-verify-snapshots pixel \\\n /tmp/jerboa-qt-huihui-tetris-game.png 19 19 32 42 56 \"huihui tetris empty cell pixel\" \\\n /tmp/jerboa-qt-huihui-tetris-game.png 59 79 64 180 120 \"huihui tetris active piece pixel\" \\\n /tmp/jerboa-qt-huihui-tetris-game.png 159 219 190 82 80 \"huihui tetris locked cell pixel\"\n\n# Example output:\n# pass: huihui tetris game: 240x460, 4 colors, 1526 bytes\n# huihui snapshot verification passed\n# pass: huihui tetris empty cell pixel: pixel 19,19 RGB 32,42,56\n# pass: huihui tetris active piece pixel: pixel 59,79 RGB 64,180,120\n# pass: huihui tetris locked cell pixel: pixel 159,219 RGB 190,82,80\n# huihui snapshot verification passed\n") ("id" . "jerboa-qt-png-snapshot-content-check") ("imports") + ("notes" + . + "Use this after the Jerboa Qt smoke or contract test has already pumped the event loop and written PNGs with `qt-widget-screenshot!`. A screenshot existence check alone is too weak, and a distinct-color check alone can still pass a stale renderer. The jerboa-qt implementation uses a shell wrapper plus Ruby/Zlib to parse non-interlaced 8-bit PNGs locally, then verifies width, height, byte size, distinct RGB color count, and exact RGB values for selected coordinates. Pair exact-pixel checks with a verifier-forced state so the coordinates are deterministic. Keep this as a verifier-owned guard; generated model code should only create the GUI/screenshot under test.") + ("tags" "jerboa-qt" "screenshot" "png" "verification" + "offscreen" "snapshot" "pixel") + ("title" + . + "Verify Jerboa Qt screenshots by dimensions and exact pixels")) + (("code" + . + "#!/usr/bin/env sh\n# Final saved-log validation requires the runner self-validation section:\nscripts/huihui-verify-run-log docs/runs/20260609T000000Z-00-syntax.md\n\n# Runner-internal validation before appending ## Run Log Verification uses an explicit pre-self mode:\nscripts/huihui-verify-run-log --allow-missing-self docs/runs/20260609T000000Z-00-syntax.md\n\n# Before a live call, capture a no-model readiness checkpoint. This must not invoke jcode or start/stop servers.\nscripts/huihui-readiness-report --with-preflight --output /tmp/huihui-readiness.md\n\n# Live-run timeout defaults should be recorded in command output sections:\nHUIHUI_JCODE_TIMEOUT_SECONDS=900\nHUIHUI_VERIFY_TIMEOUT_SECONDS=300\n\n# Before invoking jcode, prove the requested rung is next using the same provenance dir that will receive the log:\nHUIHUI_STATUS_RUN_DIR=docs/runs HUIHUI_STATUS_CHECK=1 scripts/huihui-status\n\n# Required saved-log evidence order:\n# 1. recognized heading, timestamped filename, and - Time metadata for the same rung\n# 2. exact rung-to-prompt/verifier/source/lib mapping\n# 3. prompt-file digest plus extracted Model Task digest\n# 4. ## Status Gate with selected-rung pending line, exact pre-run summary, Next rung, and exit 0\n# 5. ## Prerequisite Verifier for every dependent rung, including rung 01 when rung 00 is a syntax-smoke prerequisite, with timeout policy and exit 0\n# 6. ## Prompt Text with the exact Model Task body sent to jcode\n# 7. ## Model Environment with no MLX process, no TCP 8000/11434 listener, TCP 8001 present, and Model environment exit: 0\n# 8. ## Command with jcode --provider mlx2 verified ... --cwd matching the repo root\n# 9. ## Output with jcode output and timeout policy\n# 10. ## Final Verifier with runner-owned verifier output, timeout policy, and exit 0\n# 11. ## Generated Artifact Manifest with selected source/library SHA-256 digests and byte counts\n# 12. ## File Guard with File guard: ok\n# 13. ## Generated Module Audit with expected current-rung pass line and summary\n# 14. ## Post-Run Model Environment with no MLX process, no TCP 8000/11434 listener, TCP 8001 present, and Post-run model environment exit: 0\n# 15. ## Exit Status with zero jcode, verifier, artifact, file guard, audit, and post-run model-environment exits\n# 16. ## Run Log Verification naming the same log path and exit 0\n") ("id" . "huihui-jcode-run-log-contract") ("imports") + ("notes" + . + "Use a run-log validator whenever model generation is expensive, risky, or hard to reproduce. The validator should infer the rung from the first markdown heading and require the exact prompt path, prompt digest metadata, verifier target, command text, final verifier command, selected source artifact, and selected generated-library artifact for that rung; otherwise a Frankenstein log can combine evidence from different rungs and still look structurally complete. Require the saved log filename suffix to match the rung heading, require the UTC timestamp filename prefix to match `- Time:`, and bind the recorded `jcode --provider mlx2 verified ... --cwd` to the repository root so copied logs from another checkout are rejected.\n\nBefore any explicit live model call, generate a no-model readiness report that captures model-environment safety, generated audit, ladder status, the single status-allowed live command, dry-run routing, optional preflight, and git status. The live runner should reject any process command line containing `mlx`, not just known server names such as `mlx_lm.server`, `mlx_lm`, or `mlx-lm`; fake-process tests should include a generic spelling such as `python -m mlx.server`. It should also reject listeners on TCP 8000 or 11434 and require the existing 8001 client route.\n\nFor staged ladders, the live runner should ask the status command what rung is next using the same run-log provenance directory it will write to, record the accepted status output under `## Status Gate`, and refuse to invoke jcode unless the requested rung matches that `Next rung` line. The saved-log validator should also require the status gate to contain the selected rung's pending missing-source line and exact pre-run summary, not just a copied `Next rung` line.\n\nScope critical proof text to its owning markdown section and enforce section order outside fenced code blocks. Prompt requirements belong in `## Prompt Text`; pre-call safety and `Model environment exit: 0` belong in `## Model Environment`; artifact digests belong in `## Generated Artifact Manifest`; audit summaries belong in `## Generated Module Audit`; post-run safety and `Post-run model environment exit: 0` belong in `## Post-Run Model Environment`. Close each fenced block before the next `##` heading so later sections are independently parseable.\n\nAfter jcode returns, independently re-run the selected verifier, record the selected generated artifact digests, run a file guard, run the generated-module audit, then run `scripts/huihui-check-model-env` again. This post-run model-environment proof catches a model call that leaves MLX or a listener on TCP 8000/11434 behind. Include the post-run check in the aggregate exit-status line and fail the live run if it exits nonzero. Also fail the live run when jcode exits nonzero even if it wrote artifacts and the final verifier would pass; the saved log should preserve that model output and show a nonzero jcode slot in `## Exit Status`, which the validator rejects.\n\nTests can exercise the whole path with fake ps/lsof/jcode/make binaries so preflight stays non-model and deterministic. Include negative fixtures for bad headers, filename/header rung mismatch, malformed timestamps, mismatched prompt text or prompt digest, malformed artifact digests, mismatched command cwd, missing/nonzero prerequisite proof, missing status-gate evidence, mismatched status run directory, mismatched self-validation log path, reordered section evidence, proof text moved outside its owning section, missing pre-call or post-run environment exit proof, missing timeout policy, mismatched generated-audit summary/pass line, stale prompt/source/library provenance, out-of-sequence requested rungs, generic `mlx` command lines, nonzero jcode exits after writing artifacts, hanging jcode, unexpected file writes, and fake jcode that succeeds but leaves TCP 11434 listening afterward.") + ("tags" "huihui" "jcode" "run-log" "verification" + "prompt-text" "artifact-manifest" "rung-mapping" + "prerequisite" "self-validation" "model-env" + "model-env-exit" "post-run-model-env" + "post-run-env-provenance" "timeout" "readiness-report" + "status-sequencing" "readiness-gate" + "status-pending-provenance" "ordered-evidence" + "section-scoped" "run-dir-provenance" "filename-provenance" + "prompt-provenance" "prompt-text-provenance" + "self-path-provenance" "timestamp-provenance" + "artifact-digest-provenance" "command-cwd-provenance" + "nonzero-exit" "mlx") + ("title" + . + "Verifier-owned run-log contract for Huihui jcode live runs")) + (("code" + . + "#!/usr/bin/env sh\n# Static checks to run before trusting a behavioral contract for AI-generated\n# Jerboa code. These catch common Racket/Gerbil/Clojure, Qt-lifecycle,\n# wrong-dialect API, and verifier-owned output hallucinations seen in generated\n# Tetris code.\n\nfail=0\n\nfail_msg() {\n file=$1\n message=$2\n printf 'FAIL: %s: %s\\n' \"$file\" \"$message\" >&2\n fail=1\n}\n\nforbid_regex() {\n file=$1\n pattern=$2\n message=$3\n if grep -E -- \"$pattern\" \"$file\" >/dev/null; then\n fail_msg \"$file\" \"$message\"\n fi\n}\n\nrequire_text() {\n file=$1\n needle=$2\n message=$3\n if ! grep -F -- \"$needle\" \"$file\" >/dev/null; then\n fail_msg \"$file\" \"$message\"\n fi\n}\n\nrequire_regex() {\n file=$1\n pattern=$2\n message=$3\n if ! grep -E -- \"$pattern\" \"$file\" >/dev/null; then\n fail_msg \"$file\" \"$message\"\n fi\n}\n\ncheck_common() {\n file=$1\n require_text \"$file\" '(jerboa prelude)' 'missing (jerboa prelude) import'\n forbid_regex \"$file\" '(^|[^[:alnum:]_-])defn([^[:alnum:]_-]|$)' 'use def, not defn'\n forbid_regex \"$file\" '(^|[^[:alnum:]_-])set-[[:alnum:]_-]+!' 'Jerboa defstruct setters are field-set!, not set-field!'\n forbid_regex \"$file\" '\\(make-[[:alnum:]_-]+[^)]*[[:space:]][[:alnum:]_-]+:' 'defstruct constructors are positional, not keyword-field calls'\n forbid_regex \"$file\" '#\\{' 'do not use Clojure/hash-literal syntax'\n forbid_regex \"$file\" '\\(library([[:space:]]|\\))' 'user-facing Jerboa files must not use library forms'\n forbid_regex \"$file\" '(^|[^[:alnum:]_-])(every\\?|any\\?)([^[:alnum:]_-]|$)' 'use Jerboa every/any, not Racket-style every?/any?'\n forbid_regex \"$file\" '(^|[^[:alnum:]_-])(symbol<\\?|string-contains\\?|define-struct|environment-bound\\?|time->seconds|thread-sleep!|thread-yield|path-expand|process-status|user-info-home|the-environment|condition/report-string|make-class-type|string-subst|open-fd-pair|make-equal-hashtable|arithmetic-shift|pregexp-match)([^[:alnum:]_-]|$)' 'do not use known non-Jerboa/Gerbil/Racket API names'\n forbid_regex \"$file\" '\\(raise[[:space:]]+\"' 'do not call raise with a string; use error with a who symbol'\n forbid_regex \"$file\" 'write-file|string->path|call-with-output-file|open-output-file|delete-file' 'generated modules should not write files, screenshots, logs, or artifacts; verifier owns outputs'\n forbid_regex \"$file\" 'qt-widget-screenshot!|qt-widget-grab|qt-pixmap-save!' 'generated modules should not capture or save screenshots; verifier owns screenshots and artifacts'\n}\n\ncheck_gui_contract_module() {\n file=$1\n require_text \"$file\" '(jerboa-qt qt)' 'missing (jerboa-qt qt) import'\n forbid_regex \"$file\" 'qt-app-create|qt-app-exec!|qt-app-quit!|qt-app-destroy!|with-qt-app|qt-widget-show!' 'verifier owns Qt app lifecycle and showing'\n forbid_regex \"$file\" 'qt-pixmap-create-blank|qt-painter-create' 'generated GUI rung should paint through the returned paint-widget painter'\n require_text \"$file\" 'qt-paint-widget-create' 'GUI rung should create a paint widget'\n require_text \"$file\" 'qt-on-paint!' 'GUI rung should register a paint callback'\n require_text \"$file\" 'qt-paint-widget-painter' 'GUI rung should draw with the paint-widget painter'\n require_text \"$file\" 'qt-painter-fill-rect!' 'GUI rung should draw visible filled rectangles'\n}\n\ncheck_key_handler() {\n file=$1\n require_text \"$file\" 'qt-on-key-press!' 'GUI rung should register a key handler'\n require_text \"$file\" 'qt-last-key-code' 'key handler should query qt-last-key-code'\n require_text \"$file\" 'QT_KEY_LEFT' 'key handler should reference QT_KEY_LEFT'\n require_text \"$file\" 'QT_KEY_RIGHT' 'key handler should reference QT_KEY_RIGHT'\n}\n\ncheck_tetris_surface() {\n file=$1\n check_common \"$file\"\n check_gui_contract_module \"$file\"\n check_key_handler \"$file\"\n require_text \"$file\" 'QT_KEY_UP' 'Tetris should reference QT_KEY_UP for rotation'\n require_text \"$file\" 'QT_KEY_DOWN' 'Tetris should reference QT_KEY_DOWN for soft drop'\n require_text \"$file\" 'QT_KEY_SPACE' 'Tetris should reference QT_KEY_SPACE for hard drop'\n for piece in I O T S Z J L; do\n require_regex \"$file\" \"(^|[^[:alnum:]_-])${piece}([^[:alnum:]_-]|$)\" \"Tetris should mention ${piece} tetromino family\"\n done\n}\n\ncheck_tetris_surface src/huihui/tetris-game.ss\n[ \"$fail\" -eq 0 ]") ("id" . "jerboa-ai-generated-syntax-tripwire-audit") + ("imports") + ("notes" + . + "Use these tripwires in verifier-owned shell audits for model-generated .ss files. They are intentionally static and cheap: behavioral tests should still compile and run the module afterward. The set-...! rule catches hallucinated setters like set-block-row! while allowing Jerboa names such as board-set!, debug-board-set!, vector-set!, and qt-widget-set-minimum-size! because those do not begin with set-. Keyword-field constructor calls like (make-block row: 0 col: 4) are invalid for Jerboa defstruct constructors, which are positional. Generated rung modules should not write files, screenshots, logs, or artifacts; the runner/verifier owns all output paths, so reject file-output calls before trusting screenshots or run logs. Also reject Qt screenshot/save APIs such as qt-widget-screenshot!, qt-widget-grab, and qt-pixmap-save! inside generated modules: these belong in verifier harnesses, not model-generated code. For generated GUI rungs, the verifier should own QApplication lifecycle and widget showing, so reject with-qt-app, qt-app-exec!, and qt-widget-show!. If the contract expects a returned paint widget, reject pixmap/manual-painter designs and require qt-paint-widget-create, qt-on-paint!, qt-paint-widget-painter, and qt-painter-fill-rect!. For keyboard-driven generated modules, require the source to register qt-on-key-press!, query qt-last-key-code, and mention the required key constants; this catches no-op key callbacks before expensive GUI tests. The wrong-dialect denylist catches high-frequency AI drift from Racket, Gerbil, Gambit, Common Lisp, and R6RS, including symbol<?, string-contains?, define-struct, environment-bound?, time->seconds, thread-sleep!, path-expand, process-status, make-equal-hashtable, arithmetic-shift, and pregexp-match. Also reject (raise \"...\") because Jerboa code should use (error 'who \"message\" irritants ...). For a Tetris rung, also require QT_KEY_UP, QT_KEY_DOWN, QT_KEY_SPACE, and visible mentions of all seven tetromino symbols I/O/T/S/Z/J/L. Static symbol mentions are not a behavioral proof; pair them with contract tests for rotation, collision, hard/soft drop, spawn variety, and line clearing.") + ("tags" "jerboa" "ai-generated" "audit" "syntax" "qt" + "wrong-dialect" "output") + ("title" + . + "Static audit tripwires for AI-generated Jerboa syntax mistakes")) + (("code" + . + "(import (jerboa prelude))\n(import (huihui tetris-board))\n\n(def square-cells\n (list (list 0 0) (list 1 0) (list 0 1) (list 1 1)))\n\n(def t-cells\n (list (list 0 0) (list 1 0) (list 2 0) (list 1 1)))\n\n(def (check expr message)\n (unless expr\n (error 'tetris-board-contract \"~a\" message)))\n\n(def (check-equal got expected message)\n (unless (equal? got expected)\n (error 'tetris-board-contract \"~a: expected ~s, got ~s\" message expected got)))\n\n(def (fill-row! board row value)\n (dotimes (col board-width)\n (board-set! board col row value)))\n\n(def (row-empty? board row)\n (let loop ((col 0))\n (or (= col board-width)\n (and (not (board-ref board col row))\n (loop (+ col 1))))))\n\n;; Core assertions for AI-generated board modules:\n(check-equal board-width 10 \"board width\")\n(check-equal board-height 20 \"board height\")\n(check-equal (board-index 9 19) 199 \"row-major bottom-right\")\n(check (not (in-bounds? 10 0)) \"column past width\")\n\n(def board (make-board))\n(place! board t-cells 2 3 'T)\n(check-equal (board-ref board 2 3) 'T \"translated T left cell\")\n(check-equal (board-ref board 3 4) 'T \"translated T stem cell\")\n(check (not (board-ref board 2 4)) \"adjacent empty cell remains empty\")\n\n(def clear-board (make-board))\n(fill-row! clear-board 15 'A)\n(fill-row! clear-board 18 'B)\n(board-set! clear-board 1 14 'M14)\n(board-set! clear-board 2 16 'M16)\n(board-set! clear-board 3 17 'M17)\n(board-set! clear-board 4 19 'M19)\n(check-equal (clear-full-lines clear-board) 2 \"separated clear count\")\n(check-equal (board-ref clear-board 4 19) 'M19 \"row below clears stays\")\n(check-equal (board-ref clear-board 3 18) 'M17 \"between row shifts once\")\n(check-equal (board-ref clear-board 1 16) 'M14 \"above both clears shifts twice\")\n(check (row-empty? clear-board 0) \"top refill row empty\")\n(displayln \"tetris board contract passed\")\n") ("id" . "jerboa-tetris-board-contract-gate") + ("imports" "(jerboa prelude)") + ("notes" + . + "Use this as a fixed verifier-owned contract before adding Qt. It catches common generated-code mistakes that a shallow board test misses: swapped row/column indexing, translated piece cells written to the wrong coordinates, row-full? behavior, and clear-full-lines implementations that only clear adjacent or bottom rows. Keep the implementation module generated, but keep this contract stable in the repo. Pair it with a temporary reference-module self-test so expected-red failures are known to be caused by the generated module being absent or wrong.") + ("tags" "jerboa" "tetris" "contract" "generated-module" + "board" "verification") + ("title"