| name | spacecraft-chez-guidelines | ||||
|---|---|---|---|---|---|
| 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. | ||||
| license | GPL-3.0-or-later | ||||
| metadata |
|
Maintainer: Mohamed Hammad | Contact: Mohamed.Hammad@SpacecraftSoftware.org Copyright: (C) 2026 Mohamed Hammad & Spacecraft Software | License: GPL-3.0-or-later Website: https://Construct.SpacecraftSoftware.org/
Write Chez Scheme that is functional by default, memory-safe by default, and
concurrent via hand-built message-passing over real threads. This skill encodes the
decisions a Chez expert makes automatically so you don't fall back on generic Lisp habits
or — the more dangerous trap — Guile habits. Chez is R6RS-rooted, natively AOT-compiled
(Dybvig's nanopass backend, precise generational GC), and ships no CSP/Fibers layer and
no built-in match. Its module system, FFI, threads, and compiler are its own.
Assume Chez Scheme 9.5+ with the threaded build (--threads) unless told otherwise.
Assume R6RS libraries are the unit of code and Akku is the package manager.
Not Guile. If you catch yourself writing
(use-modules …),(srfi srfi-1),spawn-fiber,run-fibers,(ice-9 match), or(ice-9 exceptions), stop — those are Guile. The Chez equivalents are(import …),(srfi :1),fork-thread+ a mailbox you build, amatchlibrary you bring, and R6RSguard. When in doubt, read the references.
- Pure first. Functions take values and return values; push side effects (I/O, mutation, time, randomness) to the edges. A function that both computes and mutates is two functions in a trenchcoat — split them. Chez's persistent-friendly values and precise GC make this cheap.
optimize-level 2is the law;optimize-level 3is Chez'sunsafe. At level 3 the compiler omits type and bounds checks and a wrong assumption is undefined behaviour — memory corruption, not an exception. Treat level 3 exactly like Rustunsafe: never the global default, opt-in per-module, justified, reviewed, confined, and recorded in the Standard §3.1 memory-safety exemption. Reach speed with safe code, type hints, and allocation discipline first. (Seereferences/ffi-and-build.md.)- Concurrency is hand-built on threads — there is no Fibers. Chez gives you OS threads sharing one precise-GC heap (no GIL), plus mutexes and condition variables, and nothing higher. Default to immutable data shared read-only across threads; when you need message-passing, build a small mailbox/channel on mutex + condition (the references give you the code). Do not reach for a CSP scheduler — Chez has none to reach for.
- Tail calls are guaranteed (R6RS). Any recursion over unbounded data must be in tail
position — a named
letwith an accumulator, orfold-left(iterative), neverfold-rightor hand recursion on big inputs. - Hygiene only.
syntax-rulesfor pattern macros,syntax-casewhen you must bend hygiene deliberately. There is nodefine-macroto misuse here — keep it that way. - R6RS libraries are the unit; Akku is the package manager. One concern per library,
explicit
export/import, dependencies declared inAkku.manifestand pinned in the lockfile. SRFI-1,match, and SRFI-64 are Akku packages, not built-ins.
Many I/O tasks / fan-out? → a thread pool draining a mailbox you build (mutex+condition)
CPU-bound parallel compute? → fork-thread over partitions of IMMUTABLE data; collect via a results mailbox
Backpressure between stages? → a bounded channel (mutex + two conditions: not-full / not-empty)
Wait with a deadline? → condition-wait with a timeout, re-checking the predicate in a loop
Lazy, compute-once value? → (delay …) / (force …) [R6RS]
Per-thread context without globals? → (make-thread-parameter …) [Chez]
There is no choice-operation, no select over channels, no green-thread scheduler. You
compose timeouts out of condition-wait deadlines. For the two-process Majestic client
(PRD #3b) this is exactly right: a modest main loop + worker threads + one mailbox, while the
heavy parallelism lives in the Rust engine across the socket.
Before writing Chez code:
- Confirm the build. Threaded (
--threads) if any concurrency is involved; note the target (native AOT image vs. script). For a shipped binary, plan whole-program optimization (references/ffi-and-build.md). - Confirm the model. Functional/sequential, or concurrent? If concurrent, run the decision tree — and remember you are building the abstraction, not importing it.
- Identify the effects (I/O, mutation, FFI, time). Isolate them so the core stays pure and testable.
- Load the relevant reference. Read the matching
references/file for exact forms, library names, and pitfalls. Do not write Chez from memory of Guile — the module names, the FFI, and the concurrency primitives all differ. - Pick the packages. Most functional Chez pulls in
(srfi :1)and amatchlibrary; tests pull in SRFI-64. Declare them inAkku.manifest.
Read the file that matches the task; each is concise and worth reading in full when relevant.
references/functional.md— R6RS libraries &import, pure functions, R6RS list ops- SRFI-1 (Akku), tail-recursion patterns, a brought-in
match, R6RSdefine-record-type,syntax-rules/syntax-casehygiene, R6RS conditions +guard, parameters, and Akku packaging. Read for any non-trivial functional Chez.
- SRFI-1 (Akku), tail-recursion patterns, a brought-in
references/concurrent.md— The no-Fibers reality;fork-thread, mutexes, condition variables; an exception-safewith-lockmacro; building a mailbox and a bounded channel; a worker pool; a timeout pattern; CPU parallelism over immutable data;make-thread-parameter. Read for anything concurrent.references/ffi-and-build.md— The FFI (load-shared-object,foreign-procedure,foreign-callable,define-ftype,foreign-alloc/lock-object) with the boundary memory-safety discipline; the AOT compilation model (compile-program,compile-whole-program/wpo, boot files); andoptimize-levelas the safety lever. Read for any C binding (e.g. PRD #3b's TwinScrew) or any "make it fast / ship a binary" task.
Inline because they're constant. For anything beyond them, read the references.
Tail-recursive accumulation — never a non-tail loop over unbounded data:
(define (sum xs)
(let loop ((xs xs) (acc 0))
(if (null? xs) acc
(loop (cdr xs) (+ acc (car xs)))))) ; or: (fold-left + 0 xs)Exception-safe locking — own a with-lock; Chez has no with-mutex:
(define-syntax with-lock
(syntax-rules ()
((_ m body ...)
(dynamic-wind
(lambda () (mutex-acquire m))
(lambda () body ...)
(lambda () (mutex-release m)))))) ; releases even on exception / escapeBare mutex-acquire/mutex-release leaks the lock if the body raises or a continuation
escapes — never write them unguarded.
Message-passing over shared mutation — build the mailbox once, then pass values:
;; full implementation in references/concurrent.md
(define mb (make-mailbox))
(fork-thread (lambda () (mailbox-put! mb (work))))
(consume (mailbox-take! mb)) ; blocks on a condition until a value arrivesR6RS records, immutable by default:
(define-record-type point
(fields (immutable x) (immutable y)))
(point-x (make-point 3 4)) ; => 3Per-thread context, not globals:
(define current-conn (make-thread-parameter #f))
(parameterize ((current-conn c)) (run-query …))- Every source file opens with a Spacecraft SPDX header (
SPDX-FileCopyrightText,SPDX-License-Identifier: GPL-3.0-or-later) and is REUSE-clean. - Naming:
kebab-case; predicates end in?(prime?); mutators/effectful procedures end in!(set-car!,mailbox-put!). Type/record constructors aremake-…. - Avoid
set!. If you need mutable state, isolate it behind a small interface, a record field touched only through accessors, or a mailbox. - Prefer named
letloops overdo. Preferfold-left/for-eachcombinators over manual recursion when iterating. - One R6RS library per concern; explicit exports; import
(rnrs)and only the(chezscheme)pieces you use. SRFI imports use the R6RS colon form:(import (srfi :1)). - Comments:
;inline,;;block,;;;section header,;;;;file header. - Timestamps and dates are ISO 8601 / UTC (Steelbore Standard).
- Reaching for Guile —
(use-modules …),(srfi srfi-1),spawn-fiber,run-fibers,(ice-9 match),(ice-9 exceptions),with-mutex. None exist in Chez. Use(import …),(srfi :1), threads + a mailbox, amatchlibrary, R6RSguard, and your ownwith-lock. (optimize-level 3)globally — silently disables safety checks; a single wrong type or out-of-bounds index becomes memory corruption. The top safety pitfall; gate it likeunsafe.- Bare
mutex-acquire/mutex-releasewithoutdynamic-wind→ leaked lock on any exception orcall/ccescape → deadlock. condition-waitwithout a predicate loop → spurious-wakeup bugs. Always(let loop () (unless (ready?) (condition-wait cv m) (loop))).- Non-tail recursion (or
fold-right) on big data → stack overflow. Usefold-left/ a tail loop. - Assuming a rich stdlib — there is no built-in
match; bring one. ((chezscheme)does provideformat,printf, and pretty-printing.) - Handing a Scheme object's address to C without
lock-object→ the precise GC may move or collect it. Lock for the call's duration, then unlock; free what youforeign-alloc. - Holding a lock across a blocking call or long compute → contention/deadlock. Keep critical sections tiny; never block while locked.
— Built by Spacecraft Software —