|
| 1 | +--- |
| 2 | +name: spacecraft-chez-guidelines |
| 3 | +description: Write idiomatic, functional, safe, concurrent Chez Scheme following Spacecraft Software standards. Use whenever writing, reviewing, or debugging Chez Scheme — any mention of Chez, Petite Chez, R6RS libraries, `define-record-type`, `syntax-case`/`syntax-rules`, the `foreign-procedure`/`load-shared-object`/`define-ftype` FFI, `fork-thread`/`make-mutex`/`make-condition` concurrency, `optimize-level`, `compile-program`/whole-program optimization, boot files, the nanopass framework, or Akku. Trigger even when implicit, e.g. "build a worker pool in Chez", "bind this C library from Chez", "make this Chez program faster", or "is optimize-level 3 safe?". Do NOT trigger for GNU Guile (use `spacecraft-guile-guidelines` — its modules, Fibers, and FFI differ), nor Racket, Clojure, Common Lisp, or other Schemes/Lisps; Chez is R6RS-rooted with its own threads, FFI, and AOT compiler, so generic Scheme or Guile advice gets it wrong. Prefer this over generic Scheme advice for any Chez work. |
| 4 | +license: GPL-3.0-or-later |
| 5 | +metadata: |
| 6 | + maintainer: Mohamed Hammad <Mohamed.Hammad@SpacecraftSoftware.org> |
| 7 | + website: https://Construct.SpacecraftSoftware.org/ |
| 8 | +--- |
| 9 | + |
| 10 | +# Chez Scheme: Functional, Safe & Concurrent Programming |
| 11 | + |
| 12 | +**Maintainer:** Mohamed Hammad | **Contact:** [Mohamed.Hammad@SpacecraftSoftware.org](mailto:Mohamed.Hammad@SpacecraftSoftware.org) |
| 13 | +**Copyright:** (C) 2026 Mohamed Hammad & Spacecraft Software | **License:** GPL-3.0-or-later |
| 14 | +**Website:** [https://Construct.SpacecraftSoftware.org/](https://Construct.SpacecraftSoftware.org/) |
| 15 | + |
| 16 | +Write Chez Scheme that is **functional by default**, **memory-safe by default**, and |
| 17 | +**concurrent via hand-built message-passing over real threads**. This skill encodes the |
| 18 | +decisions a Chez expert makes automatically so you don't fall back on generic Lisp habits |
| 19 | +or — the more dangerous trap — **Guile habits**. Chez is R6RS-rooted, natively AOT-compiled |
| 20 | +(Dybvig's nanopass backend, precise generational GC), and ships *no* CSP/Fibers layer and |
| 21 | +*no* built-in `match`. Its module system, FFI, threads, and compiler are its own. |
| 22 | + |
| 23 | +Assume **Chez Scheme 9.5+** with the **threaded** build (`--threads`) unless told otherwise. |
| 24 | +Assume R6RS libraries are the unit of code and **Akku** is the package manager. |
| 25 | + |
| 26 | +> **Not Guile.** If you catch yourself writing `(use-modules …)`, `(srfi srfi-1)`, |
| 27 | +> `spawn-fiber`, `run-fibers`, `(ice-9 match)`, or `(ice-9 exceptions)`, stop — those are |
| 28 | +> Guile. The Chez equivalents are `(import …)`, `(srfi :1)`, `fork-thread` + a mailbox you |
| 29 | +> build, a `match` library you bring, and R6RS `guard`. When in doubt, read the references. |
| 30 | +
|
| 31 | +## Core stance |
| 32 | + |
| 33 | +1. **Pure first.** Functions take values and return values; push side effects (I/O, |
| 34 | + mutation, time, randomness) to the edges. A function that both computes and mutates is |
| 35 | + two functions in a trenchcoat — split them. Chez's persistent-friendly values and precise |
| 36 | + GC make this cheap. |
| 37 | +2. **`optimize-level 2` is the law; `optimize-level 3` is Chez's `unsafe`.** At level 3 the |
| 38 | + compiler omits type and bounds checks and a wrong assumption is undefined behaviour — |
| 39 | + memory corruption, not an exception. Treat level 3 exactly like Rust `unsafe`: never the |
| 40 | + global default, opt-in per-module, justified, reviewed, confined, and recorded in the |
| 41 | + Standard §3.1 memory-safety exemption. Reach speed with safe code, type hints, and |
| 42 | + allocation discipline first. (See `references/ffi-and-build.md`.) |
| 43 | +3. **Concurrency is hand-built on threads — there is no Fibers.** Chez gives you OS threads |
| 44 | + sharing one precise-GC heap (no GIL), plus mutexes and condition variables, and *nothing* |
| 45 | + higher. Default to immutable data shared read-only across threads; when you need |
| 46 | + message-passing, build a small mailbox/channel on mutex + condition (the references give |
| 47 | + you the code). Do not reach for a CSP scheduler — Chez has none to reach for. |
| 48 | +4. **Tail calls are guaranteed (R6RS).** Any recursion over unbounded data must be in tail |
| 49 | + position — a named `let` with an accumulator, or `fold-left` (iterative), never |
| 50 | + `fold-right` or hand recursion on big inputs. |
| 51 | +5. **Hygiene only.** `syntax-rules` for pattern macros, `syntax-case` when you must bend |
| 52 | + hygiene deliberately. There is no `define-macro` to misuse here — keep it that way. |
| 53 | +6. **R6RS libraries are the unit; Akku is the package manager.** One concern per library, |
| 54 | + explicit `export`/`import`, dependencies declared in `Akku.manifest` and pinned in the |
| 55 | + lockfile. SRFI-1, `match`, and SRFI-64 are Akku packages, not built-ins. |
| 56 | + |
| 57 | +## Decision tree: which concurrency tool? |
| 58 | + |
| 59 | +``` |
| 60 | +Many I/O tasks / fan-out? → a thread pool draining a mailbox you build (mutex+condition) |
| 61 | +CPU-bound parallel compute? → fork-thread over partitions of IMMUTABLE data; collect via a results mailbox |
| 62 | +Backpressure between stages? → a bounded channel (mutex + two conditions: not-full / not-empty) |
| 63 | +Wait with a deadline? → condition-wait with a timeout, re-checking the predicate in a loop |
| 64 | +Lazy, compute-once value? → (delay …) / (force …) [R6RS] |
| 65 | +Per-thread context without globals? → (make-thread-parameter …) [Chez] |
| 66 | +``` |
| 67 | + |
| 68 | +There is no `choice-operation`, no `select` over channels, no green-thread scheduler. You |
| 69 | +compose timeouts out of `condition-wait` deadlines. For the two-process Majestic client |
| 70 | +(PRD #3b) this is exactly right: a modest main loop + worker threads + one mailbox, while the |
| 71 | +heavy parallelism lives in the Rust engine across the socket. |
| 72 | + |
| 73 | +## Workflow |
| 74 | + |
| 75 | +Before writing Chez code: |
| 76 | + |
| 77 | +1. **Confirm the build.** Threaded (`--threads`) if any concurrency is involved; note the |
| 78 | + target (native AOT image vs. script). For a shipped binary, plan whole-program |
| 79 | + optimization (`references/ffi-and-build.md`). |
| 80 | +2. **Confirm the model.** Functional/sequential, or concurrent? If concurrent, run the |
| 81 | + decision tree — and remember you are *building* the abstraction, not importing it. |
| 82 | +3. **Identify the effects** (I/O, mutation, FFI, time). Isolate them so the core stays pure |
| 83 | + and testable. |
| 84 | +4. **Load the relevant reference.** Read the matching `references/` file for exact forms, |
| 85 | + library names, and pitfalls. Do **not** write Chez from memory of Guile — the module |
| 86 | + names, the FFI, and the concurrency primitives all differ. |
| 87 | +5. **Pick the packages.** Most functional Chez pulls in `(srfi :1)` and a `match` library; |
| 88 | + tests pull in SRFI-64. Declare them in `Akku.manifest`. |
| 89 | + |
| 90 | +## Reference files |
| 91 | + |
| 92 | +Read the file that matches the task; each is concise and worth reading in full when relevant. |
| 93 | + |
| 94 | +- **`references/functional.md`** — R6RS libraries & `import`, pure functions, R6RS list ops |
| 95 | + + SRFI-1 (Akku), tail-recursion patterns, a brought-in `match`, R6RS `define-record-type`, |
| 96 | + `syntax-rules`/`syntax-case` hygiene, R6RS conditions + `guard`, parameters, and Akku |
| 97 | + packaging. Read for any non-trivial functional Chez. |
| 98 | +- **`references/concurrent.md`** — The no-Fibers reality; `fork-thread`, mutexes, condition |
| 99 | + variables; an exception-safe `with-lock` macro; building a mailbox and a bounded channel; |
| 100 | + a worker pool; a timeout pattern; CPU parallelism over immutable data; `make-thread-parameter`. |
| 101 | + Read for anything concurrent. |
| 102 | +- **`references/ffi-and-build.md`** — The FFI (`load-shared-object`, `foreign-procedure`, |
| 103 | + `foreign-callable`, `define-ftype`, `foreign-alloc`/`lock-object`) with the boundary |
| 104 | + memory-safety discipline; the AOT compilation model (`compile-program`, |
| 105 | + `compile-whole-program`/wpo, boot files); and **`optimize-level` as the safety lever**. |
| 106 | + Read for any C binding (e.g. PRD #3b's TwinScrew) or any "make it fast / ship a binary" task. |
| 107 | + |
| 108 | +## Non-negotiable idioms (quick reference) |
| 109 | + |
| 110 | +Inline because they're constant. For anything beyond them, read the references. |
| 111 | + |
| 112 | +**Tail-recursive accumulation** — never a non-tail loop over unbounded data: |
| 113 | +```scheme |
| 114 | +(define (sum xs) |
| 115 | + (let loop ((xs xs) (acc 0)) |
| 116 | + (if (null? xs) acc |
| 117 | + (loop (cdr xs) (+ acc (car xs)))))) ; or: (fold-left + 0 xs) |
| 118 | +``` |
| 119 | + |
| 120 | +**Exception-safe locking — own a `with-lock`; Chez has no `with-mutex`:** |
| 121 | +```scheme |
| 122 | +(define-syntax with-lock |
| 123 | + (syntax-rules () |
| 124 | + ((_ m body ...) |
| 125 | + (dynamic-wind |
| 126 | + (lambda () (mutex-acquire m)) |
| 127 | + (lambda () body ...) |
| 128 | + (lambda () (mutex-release m)))))) ; releases even on exception / escape |
| 129 | +``` |
| 130 | +Bare `mutex-acquire`/`mutex-release` leaks the lock if the body raises or a continuation |
| 131 | +escapes — never write them unguarded. |
| 132 | + |
| 133 | +**Message-passing over shared mutation** — build the mailbox once, then pass values: |
| 134 | +```scheme |
| 135 | +;; full implementation in references/concurrent.md |
| 136 | +(define mb (make-mailbox)) |
| 137 | +(fork-thread (lambda () (mailbox-put! mb (work)))) |
| 138 | +(consume (mailbox-take! mb)) ; blocks on a condition until a value arrives |
| 139 | +``` |
| 140 | + |
| 141 | +**R6RS records, immutable by default:** |
| 142 | +```scheme |
| 143 | +(define-record-type point |
| 144 | + (fields (immutable x) (immutable y))) |
| 145 | +(point-x (make-point 3 4)) ; => 3 |
| 146 | +``` |
| 147 | + |
| 148 | +**Per-thread context, not globals:** |
| 149 | +```scheme |
| 150 | +(define current-conn (make-thread-parameter #f)) |
| 151 | +(parameterize ((current-conn c)) (run-query …)) |
| 152 | +``` |
| 153 | + |
| 154 | +## Style rules |
| 155 | + |
| 156 | +- Every source file opens with a Spacecraft SPDX header (`SPDX-FileCopyrightText`, |
| 157 | + `SPDX-License-Identifier: GPL-3.0-or-later`) and is REUSE-clean. |
| 158 | +- Naming: `kebab-case`; predicates end in `?` (`prime?`); mutators/effectful procedures end |
| 159 | + in `!` (`set-car!`, `mailbox-put!`). Type/record constructors are `make-…`. |
| 160 | +- Avoid `set!`. If you need mutable state, isolate it behind a small interface, a record |
| 161 | + field touched only through accessors, or a mailbox. |
| 162 | +- Prefer named `let` loops over `do`. Prefer `fold-left`/`for-each` combinators over manual |
| 163 | + recursion when iterating. |
| 164 | +- One R6RS library per concern; explicit exports; import `(rnrs)` and only the `(chezscheme)` |
| 165 | + pieces you use. SRFI imports use the R6RS colon form: `(import (srfi :1))`. |
| 166 | +- Comments: `;` inline, `;;` block, `;;;` section header, `;;;;` file header. |
| 167 | +- Timestamps and dates are ISO 8601 / UTC (Steelbore Standard). |
| 168 | + |
| 169 | +## Common pitfalls to actively avoid |
| 170 | + |
| 171 | +- **Reaching for Guile** — `(use-modules …)`, `(srfi srfi-1)`, `spawn-fiber`, `run-fibers`, |
| 172 | + `(ice-9 match)`, `(ice-9 exceptions)`, `with-mutex`. None exist in Chez. Use `(import …)`, |
| 173 | + `(srfi :1)`, threads + a mailbox, a `match` library, R6RS `guard`, and your own `with-lock`. |
| 174 | +- **`(optimize-level 3)` globally** — silently disables safety checks; a single wrong type or |
| 175 | + out-of-bounds index becomes memory corruption. The top safety pitfall; gate it like `unsafe`. |
| 176 | +- **Bare `mutex-acquire`/`mutex-release`** without `dynamic-wind` → leaked lock on any |
| 177 | + exception or `call/cc` escape → deadlock. |
| 178 | +- **`condition-wait` without a predicate loop** → spurious-wakeup bugs. Always |
| 179 | + `(let loop () (unless (ready?) (condition-wait cv m) (loop)))`. |
| 180 | +- **Non-tail recursion (or `fold-right`) on big data** → stack overflow. Use `fold-left` / a |
| 181 | + tail loop. |
| 182 | +- **Assuming a rich stdlib** — there is no built-in `match`; bring one. (`(chezscheme)` does |
| 183 | + provide `format`, `printf`, and pretty-printing.) |
| 184 | +- **Handing a Scheme object's address to C without `lock-object`** → the precise GC may move |
| 185 | + or collect it. Lock for the call's duration, then unlock; free what you `foreign-alloc`. |
| 186 | +- **Holding a lock across a blocking call or long compute** → contention/deadlock. Keep |
| 187 | + critical sections tiny; never block while locked. |
| 188 | + |
| 189 | +*— Built by Spacecraft Software —* |
0 commit comments