Add design docs for Clojure compat work (pmap/pset/csp)

ober

f60f828773e5d4ac5c6a07eb223d2916e94c6a42

diff --git a/docs/clojure-vs-jerboa.md b/docs/clojure-vs-jerboa.md
new file mode 100644
index 0000000..8615283
--- /dev/null
+++ b/docs/clojure-vs-jerboa.md
@@ -0,0 +1,1430 @@
+# Clojure vs Jerboa: Feature Gap Analysis
+
+A comprehensive inventory of what Clojure offers that Jerboa/Chez Scheme doesn't (or has in weaker form), with detailed explanations of what each feature does and why Clojure developers consider it a game-changer.
+
+Organized from most foundational to most specialized. In-progress Jerboa work noted where relevant.
+
+---
+
+## Table of Contents
+
+1. [The Philosophical Foundation](#1-the-philosophical-foundation)
+2. [Persistent Immutable Data Structures](#2-persistent-immutable-data-structures) (in progress)
+3. [Transients](#3-transients)
+4. [The Unified Sequence Abstraction](#4-the-unified-sequence-abstraction)
+5. [Lazy Sequences](#5-lazy-sequences)
+6. [Transducers](#6-transducers)
+7. [Reducers](#7-reducers)
+8. [The Four Reference Types](#8-the-four-reference-types)
+9. [Software Transactional Memory (STM)](#9-software-transactional-memory-stm)
+10. [core.async (CSP Channels)](#10-coreasync-csp-channels) (in progress)
+11. [Protocols](#11-protocols)
+12. [Multimethods](#12-multimethods)
+13. [Records and Types](#13-records-and-types)
+14. [Destructuring Everywhere](#14-destructuring-everywhere)
+15. [Metadata](#15-metadata)
+16. [Dynamic Vars and Thread-Local Binding](#16-dynamic-vars-and-thread-local-binding)
+17. [Clojure Spec](#17-clojure-spec)
+18. [Namespaces](#18-namespaces)
+19. [EDN and Tagged Literals](#19-edn-and-tagged-literals)
+20. [Reader Conditionals](#20-reader-conditionals)
+21. [Rich Number Tower](#21-rich-number-tower)
+22. [Keyword/Symbol Semantics](#22-keywordsymbol-semantics)
+23. [`loop`/`recur` and Tail Calls](#23-looprecur-and-tail-calls)
+24. [`for` Comprehensions](#24-for-comprehensions)
+25. [Delays, Futures, and Promises](#25-delays-futures-and-promises)
+26. [Memoize, Trampoline, and Friends](#26-memoize-trampoline-and-friends)
+27. [Exception Design: ex-info](#27-exception-design-ex-info)
+28. [core.match](#28-corematch)
+29. [core.logic (miniKanren)](#29-corelogic-minikanren)
+30. [Zippers](#30-zippers)
+31. [Specter](#31-specter)
+32. [Datafy / Nav](#32-datafy--nav)
+33. [clojure.walk](#33-clojurewalk)
+34. [Set Operations](#34-set-operations)
+35. [Property-Based Testing (test.check)](#35-property-based-testing-testcheck)
+36. [Component Lifecycle Libraries](#36-component-lifecycle-libraries)
+37. [REPL-Driven Development Culture](#37-repl-driven-development-culture)
+38. [Miscellaneous Small Things That Add Up](#38-miscellaneous-small-things-that-add-up)
+
+---
+
+## 1. The Philosophical Foundation
+
+Before features, Clojure's mindset:
+
+### Identity vs Value (Rich Hickey's Model)
+
+**Values** are immutable things: the number 42, the string "hello", the map `{:a 1}`. They never change. Two values that are equal are indistinguishable.
+
+**Identities** are named things that *change what value they point to* over time. A bank account isn't a number that mutates; it's an identity whose value at time T is some immutable balance.
+
+Every Clojure reference type (atom, ref, agent, var) embodies this split: the reference is the identity, swapping pointers; the value it points to is always immutable. This is why Clojure concurrency "just works" — readers can never see a half-mutated value, because values don't mutate at all. They are replaced atomically.
+
+**Why this matters for Jerboa**: Scheme has `set!` everywhere and mutable cons cells, boxes, and vectors. To get Clojure's guarantees you need to *culturally* commit to persistent structures and discipline around mutation, plus the reference types described below.
+
+### Code is Data (Homoiconicity) + The Reader
+
+All Lisps are homoiconic, but Clojure's reader ships with rich literal forms:
+- `[1 2 3]` — persistent vector
+- `{:a 1 :b 2}` — persistent map
+- `#{1 2 3}` — persistent set
+- `'(1 2 3)` — persistent list
+- `#"regex"` — compiled regex literal
+- `#inst "2026-04-10"` — tagged literal for instant
+- `#uuid "..."` — tagged UUID
+- `#_form` — reader-level comment, skips next form
+
+Jerboa has `[...]` aliased to `(...)` and has strings/numbers, but no literal map/set/vector with distinct types, no tagged literal extension mechanism for the reader.
+
+---
+
+## 2. Persistent Immutable Data Structures
+
+**Status in Jerboa**: In progress.
+
+Clojure's defining technical achievement. Four core persistent collections, all implemented via **Hash Array Mapped Tries (HAMT)** or variants, giving effectively-O(1) (really O(log32 N)) updates with **structural sharing**.
+
+### PersistentVector
+```clojure
+(def v [1 2 3 4 5])
+(conj v 6)        ; => [1 2 3 4 5 6]  — v is unchanged
+(assoc v 0 99)    ; => [99 2 3 4 5]
+(pop v)           ; => [1 2 3 4]
+(nth v 2)         ; => 3
+(subvec v 1 3)    ; => [2 3]
+```
+Implemented as a 32-way trie. Random access, append, and update are all O(log32 N) ≈ O(1) in practice. Old versions remain valid and consume negligible extra memory due to sharing.
+
+### PersistentHashMap
+```clojure
+(def m {:a 1 :b 2})
+(assoc m :c 3)           ; => {:a 1 :b 2 :c 3}
+(dissoc m :a)            ; => {:b 2}
+(get m :a)               ; => 1
+(:a m)                   ; => 1   (keywords are functions!)
+(merge m {:d 4})         ; => {:a 1 :b 2 :d 4}
+(update m :a inc)        ; => {:a 2 :b 2}
+(update-in m [:a] + 10)  ; => {:a 11 :b 2}
+```
+The workhorse. HAMT-based. The `update`/`update-in`/`assoc-in` functions let you "modify" deeply nested data trivially:
+```clojure
+(update-in state [:users 42 :profile :email] clojure.string/lower-case)
+```
+
+### PersistentHashSet
+```clojure
+(def s #{1 2 3})
+(conj s 4)          ; => #{1 2 3 4}
+(disj s 2)          ; => #{1 3}
+(contains? s 1)     ; => true
+(s 1)               ; => 1    (sets are functions of their members!)
+```
+
+### PersistentList / PersistentQueue
+Lists are the classic singly-linked list but immutable. Queues are O(1) persistent FIFO: `(conj q x)` adds to tail, `(pop q)` removes from head, `(peek q)` sees head.
+
+### Why This Is a Game-Changer
+
+1. **Fearless sharing**: Pass a map to another thread, another function, into a channel — no defensive copies, ever.
+2. **Time travel / undo**: Keep a list of previous states. Each takes trivial extra memory.
+3. **Reasoning**: If you have a reference to a value, nobody can change it from under you.
+4. **Compositional updates**: `assoc-in`, `update-in`, `merge-with`, `get-in`.
+5. **Equality by value**: Two maps are `=` iff they contain the same entries. Same for nested structures. Hashes are structural.
+
+### Jerboa Gap
+Chez's built-in hashtables are mutable, and `eq?`/`equal?` comparison of them works but there's no structural sharing. Lists are persistent-ish (cons cells), but conjugate, assoc, update-in style operations don't exist. No persistent vector, map, set, or queue with HAMT characteristics. You're building this — it's the foundation for almost everything else.
+
+---
+
+## 3. Transients
+
+A sidekick to persistent data: **transients** are a mutable "building mode" for persistent collections.
+
+```clojure
+(defn build-big-vec [n]
+  (persistent!
+    (reduce (fn [v i] (conj! v i))
+            (transient [])
+            (range n))))
+```
+
+`transient` turns a persistent collection into an ephemeral mutable one — *but only the thread that created it can use it*. You batch mutations (`conj!`, `assoc!`, `dissoc!`), then call `persistent!` to freeze it back. The internal structure is shared with the result, so this is much faster than repeated `conj`/`assoc` when building large collections in a loop.
+
+The genius: transients give you mutability's performance without losing immutability's semantics, as long as you don't leak the transient across threads.
+
+**Jerboa gap**: No persistent structures yet, so no transients. When you build them, transients should be part of the design from day one — they're how Clojure avoids the "but immutable is slow!" critique.
+
+---
+
+## 4. The Unified Sequence Abstraction
+
+`seq` is Clojure's universal iteration abstraction. Anything "seqable" — list, vector, map, set, string, Java iterable, lazy seq, channel poll — can be treated through the same API:
+
+```clojure
+(first coll)    ; first element or nil
+(rest coll)     ; remaining elements as seq
+(next coll)     ; like rest but nil if empty
+(cons x coll)   ; prepend
+(seq coll)      ; coerce to seq, nil if empty
+```
+
+Every higher-order operation — `map`, `filter`, `reduce`, `take`, `drop`, `take-while`, `drop-while`, `partition`, `partition-by`, `partition-all`, `interpose`, `interleave`, `mapcat`, `iterate`, `cycle`, `repeat`, `repeatedly`, `range`, `distinct`, `dedupe`, `frequencies`, `group-by`, `sort-by`, `keep`, `keep-indexed`, `map-indexed`, `reductions`, `tree-seq`, `flatten`, `zipmap`, `split-at`, `split-with` — takes and/or returns seqs.
+
+Because of this uniformity, once you learn 40 sequence functions, they work on *everything*.
+
+**Jerboa has**: `for/collect`, `for/fold`, `map`, `filter`, `flatten`, `unique`, `take`, `drop`, `every`, `any`, `filter-map`, `group-by`, `zip`, `frequencies`, `partition`, `interleave`, `mapcat`, `distinct`, `keep`, `split-at`, `append-map`, `snoc`. Good coverage but not a single unified abstraction — lists, vectors, hash tables, and strings each have their own APIs. Iteration via `in-list`/`in-vector`/`in-hash-keys` in `for` is closest to the seq idea but is a macro-time thing, not a runtime polymorphic protocol.
+
+---
+
+## 5. Lazy Sequences
+
+Sequences in Clojure are **lazy by default**. `(map f coll)` does not walk `coll`; it returns a lazy sequence that realizes elements on demand.
+
+```clojure
+(def naturals (iterate inc 0))         ; infinite!
+(take 10 naturals)                      ; => (0 1 2 3 4 5 6 7 8 9)
+
+(def squares (map #(* % %) naturals))   ; infinite, not computed
+(take 5 squares)                        ; => (0 1 4 9 16)
+
+(defn primes-from [n]
+  (cons n (lazy-seq (primes-from (+ n 1)))))  ; build your own
+```
+
+Key operators:
+- `lazy-seq` — the primitive; delays a sequence expression
+- `iterate f x` — `(x (f x) (f (f x)) ...)` infinite
+- `repeat x` / `repeat n x` — infinite or n copies
+- `repeatedly f` / `repeatedly n f` — call f on demand
+- `cycle coll` — infinite repetition of coll
+- `range`, `range n`, `range start end`, `range start end step` — optionally infinite
+
+Laziness composes naturally with `take`, `take-while`, `drop-while`. You can write algorithms that look like they process infinite data, and they do, one element at a time.
+
+**Gotchas Clojure exposes**: chunking (realizes 32 at a time for efficiency), head retention (holding the head of a lazy seq prevents GC of realized elements), and `doall`/`dorun` for forcing.
+
+**Jerboa has**: Chez's lazy library exists but isn't in the prelude. `in-producer` gives some of this via `for`. No culture of lazy-by-default; you build with eager collections and decide to go lazy. A lazy-seq macro integrated into the prelude would close much of the gap.
+
+---
+
+## 6. Transducers
+
+Introduced in Clojure 1.7, transducers are **composable algorithmic transformations decoupled from their input and output**.
+
+### The problem they solve
+
+Consider:
+```clojure
+(->> data
+     (map inc)
+     (filter even?)
+     (take 10))
+```
+
+In classical Clojure, each step produces an intermediate lazy sequence. With large data or tight loops, that's allocation overhead. Also, the same logical pipeline has to be rewritten for different contexts: sequences vs channels vs streams.
+
+### The transducer idea
+
+A transducer is a function that takes a *reducing function* and returns a new reducing function. `map`, `filter`, `take`, etc., all have **arity-1 forms** that return transducers:
+
+```clojure
+(def xf (comp (map inc) (filter even?) (take 10)))   ; a transducer
+```
+
+This `xf` is just a function composition. Now apply it in any context:
+
+```clojure
+(into [] xf data)              ; build a vector, no intermediate seqs
+(sequence xf data)             ; lazy sequence
+(transduce xf + 0 data)        ; reduce to a number
+(chan 10 xf)                   ; a core.async channel that transforms values
+(eduction xf data)             ; reusable reducible
+```
+
+The same pipeline works over eager collections, lazy seqs, channels, and reducibles. Zero intermediate collections. Compose with `comp`.
+
+### Why it's a game-changer
+
+- **Performance**: no intermediate allocations
+- **Reusability**: one pipeline, many contexts
+- **Composability**: `comp` chains them; you can build libraries of reusable transformations
+- **Channel transformation**: core.async channels can apply a transducer as values flow through — extremely elegant for stream processing
+
+### The formal definition
+
+```clojure
+;; A transducer:
+(fn [rf]                      ; takes a reducing function (acc, input -> acc)
+  (fn
+    ([] (rf))                 ; init arity
+    ([acc] (rf acc))          ; completion arity
+    ([acc input] ...)))       ; step arity
+```
+
+Most people never write raw transducers — they compose `map`, `filter`, `mapcat`, `take`, `drop`, `dedupe`, `distinct`, `partition-by`, `partition-all`, `map-indexed`, `keep`, `keep-indexed`, `cat` (catenating transducer), `halt-when`, `interpose`, `random-sample`.
+
+### Jerboa gap
+
+Jerboa has `for/fold` and `for/collect` which are macro-level fused loops — conceptually similar in that they avoid intermediate lists, but not composable values you can pass around. Building a transducer library in Jerboa would require:
+1. Agreeing on the reducing-function protocol.
+2. Providing arity-overloaded `map`/`filter`/etc. that return transducers when called without a collection.
+3. Providing `into`/`transduce`/`sequence`/`eduction` entry points.
+
+Huge payoff. Probably the single feature most worth porting after persistent data.
+
+---
+
+## 7. Reducers
+
+An earlier (2012) attempt at the same composition problem that lives on alongside transducers. `clojure.core.reducers` provides:
+
+- `r/map`, `r/filter`, `r/mapcat`, `r/take`, `r/drop` — return **reducibles** (things that know how to reduce themselves), not seqs
+- `r/fold` — parallel reduce via fork/join, splits the work across cores automatically for supported collections (vectors and maps)
+- `r/reduce`, `r/foldcat`
+
+```clojure
+(require '[clojure.core.reducers :as r])
+(r/fold + (r/map inc (r/filter odd? big-vector)))    ; parallel!
+```
+
+The `fold` bit is the key — Clojure automatically parallelizes the reduction for persistent vectors/maps using a divide-and-conquer `combine-fn`.
+
+**Jerboa gap**: No parallel fold. `pmap` exists (you mentioned fixing it recently), but no reducer-level parallelism that automatically splits collections. Chez has threads; the primitives are there.
+
+---
+
+## 8. The Four Reference Types
+
+Clojure's unified concurrency model. All four embody identity-vs-value, differing on (a) sync vs async and (b) coordinated vs uncoordinated.
+
+|             | **Uncoordinated**       | **Coordinated** |
+|-------------|-------------------------|-----------------|
+| **Sync**    | `atom`                  | `ref` (STM)     |
+| **Async**   | `agent`                 | —               |
+
+Plus `var` for thread-local dynamic binding.
+
+### Atom
+
+**Status in Jerboa**: Recently aliased in `(std misc atom)` and re-exported from the prelude.
+
+```clojure
+(def counter (atom 0))
+@counter                          ; deref: 0
+(swap! counter inc)               ; atomically: 1
+(swap! counter + 10)              ; 11
+(reset! counter 0)                ; 0
+(compare-and-set! counter 0 42)   ; CAS
+```
+
+Uses compare-and-swap. `swap!`'s function may retry if another thread raced — so the function **must be pure**. No coordination with other refs.
+
+Also:
+- `add-watch` — register a callback on change
+- `remove-watch`
+- `set-validator!` — reject invalid values
+
+### Ref
+
+The STM reference. See section [9](#9-software-transactional-memory-stm).
+
+### Agent
+
+An identity that updates asynchronously:
+```clojure
+(def a (agent 0))
+(send a inc)                      ; queue a fn; returns immediately
+(send-off a slow-io-fn)           ; for blocking IO, uses unbounded pool
+@a                                ; may or may not have applied yet
+(await a)                         ; block until all queued actions done
+```
+
+Key properties:
+- Actions are queued and applied serially per-agent (no races on a single agent).
+- Errors set the agent to a failed state; `agent-error`, `restart-agent`.
+- `send` uses a fixed thread pool; `send-off` uses unbounded (for IO-bound work).
+- Combines naturally with STM: within a `dosync`, `send` is deferred until commit.
+
+### Var and Dynamic Binding
+
+See section [16](#16-dynamic-vars-and-thread-local-binding).
+
+**Jerboa gap**: Atoms just arrived. Agents and refs are the big missing pieces. Building agents on top of Chez threads + a work queue is straightforward. STM is harder.
+
+---
+
+## 9. Software Transactional Memory (STM)
+
+Clojure's most distinctive concurrency primitive. Coordinated, synchronous updates across multiple refs with ACI guarantees (no D — STM isn't durable).
+
+```clojure
+(def account-a (ref 100))
+(def account-b (ref 0))
+
+(defn transfer [from to amount]
+  (dosync
+    (alter from - amount)
+    (alter to + amount)))
+
+(transfer account-a account-b 30)
+```
+
+Inside `dosync`:
+- Reads use `@ref` or `(deref ref)`.
+- Writes use `alter`, `ref-set`, or `commute`.
+- If another transaction committed to a ref you read or wrote, your transaction **automatically retries**.
+- All updates commit atomically or none do.
+- The body must be **side-effect free** (it may be rerun). Use `agents` or explicit `io!` blocks for side effects.
+
+### `commute` vs `alter`
+- `alter` requires that the ref hasn't changed since you read it. Strict ordering.
+- `commute` lets concurrent transactions interleave if the update is commutative (e.g. `+`, `conj`). Can improve throughput dramatically.
+
+### `ensure`
+- Prevents another transaction from modifying a ref you read, without actually writing to it. For when a read needs to be protected.
+
+### Why it matters
+
+Lock-based concurrency is notoriously error-prone: deadlocks, lock ordering, forgotten unlocks, coarse-vs-fine granularity tradeoffs. STM sidesteps all of that: you write the transaction as if you had exclusive access; the runtime ensures isolation via optimistic concurrency and retry.
+
+**Cost**: transactions can't do IO (or must be idempotent), and contention leads to retry churn. But for "update these five refs consistently" problems, nothing is as ergonomic.
+
+**Jerboa gap**: None of this. Chez has mutexes and condition variables. Building STM requires:
+1. Version counters on refs.
+2. Transaction-local read/write sets.
+3. A commit protocol with conflict detection.
+4. Retry semantics.
+
+It's a significant engineering project, but the abstraction is well-specified.
+
+---
+
+## 10. core.async (CSP Channels)
+
+**Status in Jerboa**: In progress.
+
+Clojure's CSP-style concurrency library. Not in core but ships with Clojure projects almost universally.
+
+### The basics
+
+```clojure
+(require '[clojure.core.async :as a :refer [chan go >! <! >!! <!! close! alts!]])
+
+(def c (chan 10))                     ; buffered channel, capacity 10
+(go (>! c "hello"))                    ; async put, parks if full
+(go (println (<! c)))                  ; async take, parks if empty
+```
+
+- `chan` — unbuffered by default; `(chan n)` fixed buffer; `(chan (dropping-buffer n))`, `(chan (sliding-buffer n))`
+- `>!` / `<!` — parking put/take, only inside a `go` block
+- `>!!` / `<!!` — blocking put/take, outside go blocks
+- `put!` / `take!` — callback-based, lowest level
+- `close!` — no more puts; pending takes return `nil`
+
+### `go` blocks
+
+The magic. `go` is a macro that performs **state machine transformation** on its body, turning code with `>!`/`<!` into a continuation-passing machine that **parks** (not blocks) on a channel operation:
+
+```clojure
+(go
+  (let [x (<! c1)
+        y (<! c2)]
+    (>! c3 (+ x y))))
+```
+
+When `(<! c1)` can't immediately get a value, the go block is suspended without consuming a thread. The scheduler resumes it when `c1` has a value. This lets you have millions of "logical" concurrent workers on a small thread pool.
+
+### `alts!` — select over many channels
+
+```clojure
+(go
+  (let [[val ch] (alts! [c1 c2 c3])]
+    (println "got" val "from" ch)))
+```
+
+Also supports `(alts! [[ch val] c2])` — try to put `val` on `ch` OR take from `c2`, whichever can proceed first. `:default` option for non-blocking. `:priority` for ordering.
+
+### Pipelines
+
+- `pipe`, `pipeline`, `pipeline-async`, `pipeline-blocking` — connect channels with transducers and parallelism
+- `merge` — combine multiple channels into one
+- `mult` / `tap` — broadcast one channel to many
+- `pub` / `sub` — topic-based pub/sub
+- `mix`/`admix`/`unmix` — dynamically composable input channel
+
+### Transducer integration
+
+```clojure
+(chan 10 (comp (map inc) (filter even?)))
+```
+
+A channel can apply a transducer to values as they flow. The pipeline steps execute on the put side, inline.
+
+### Why it matters
+
+- Structured concurrent communication without shared mutable state.
+- Uniform API across "real threads" (`<!!`) and lightweight go-blocks (`<!`).
+- Composes beautifully with transducers.
+- Solves the "what if I want millions of workers" problem on a JVM with thousands of OS threads.
+
+**Jerboa gap**: In progress per user note. Chez has threads and mutexes; the hard part is the `go` macro's state machine transformation (CPS transform). Possible approaches:
+1. OS-thread-per-channel-worker (simpler, doesn't scale to millions).
+2. Delimited continuations (Chez has them via `call/1cc`).
+3. A full CPS transformation macro like Clojure's.
+
+---
+
+## 11. Protocols
+
+Interface-like polymorphism on the **first argument's type**. Like Rust traits but runtime-dispatched, and extensible from outside.
+
+```clojure
+(defprotocol Shape
+  (area [this])
+  (perimeter [this]))
+
+(defrecord Circle [radius]
+  Shape
+  (area [this] (* Math/PI radius radius))
+  (perimeter [this] (* 2 Math/PI radius)))
+
+(defrecord Rectangle [w h]
+  Shape
+  (area [this] (* w h))
+  (perimeter [this] (* 2 (+ w h))))
+
+(area (->Circle 5))        ; => 78.54...
+```
+
+### The killer feature: extend from outside
+
+```clojure
+(extend-protocol Shape
+  String
+  (area [s] (count s))
+  (perimeter [s] 0))
+```
+
+You can make **types you didn't define** (including built-in types, Java types) satisfy your protocol. This solves the **expression problem**: you can add new operations over existing types, and new types supporting existing operations, without modifying either.
+
+Also: `extend-type`, `reify` (anonymous implementation of a protocol), `satisfies?` (runtime check).
+
+### Performance
+
+Protocol dispatch is extremely fast — compiles to a type-keyed cache that becomes direct dispatch after warmup. Near-virtual-call speed.
+
+**Jerboa has**: `defmethod` on structs via `(defmethod (area (self circle)) ...)`. This is single-inheritance method dispatch tied to a struct. Clojure protocols are independent of type hierarchy and can be retrofitted onto any existing type. Jerboa's method system is closer to Gerbil/CLOS without the extensibility story.
+
+---
+
+## 12. Multimethods
+
+Where protocols dispatch on the first argument's class, **multimethods dispatch on the value of an arbitrary dispatch function**:
+
+```clojure
+(defmulti area :shape-type)
+
+(defmethod area :circle [s]
+  (* Math/PI (:radius s) (:radius s)))
+
+(defmethod area :square [s]
+  (let [side (:side s)] (* side side)))
+
+(area {:shape-type :circle :radius 5})
+```
+
+- The dispatch function can return **any value** (keyword, string, vector, anything).
+- Methods match by `isa?` — supports hierarchies!
+- `derive`, `underive`, `ancestors`, `parents`, `make-hierarchy` — user-defined taxonomies completely decoupled from class hierarchies.
+- `:default` method for fallback.
+- `prefer-method` to break ambiguities.
+
+### Dispatch on multiple things
+
+```clojure
+(defmulti collide (fn [a b] [(:type a) (:type b)]))
+(defmethod collide [:ship :asteroid] [a b] ...)
+(defmethod collide [:ship :ship] [a b] ...)
+```
+
+True multiple dispatch (CLOS-style).
+
+### Why it matters
+
+Separates "how do I choose which code runs" from "what code runs". Can model taxonomies that cross-cut your type system. Overkill for many things (hence protocols are preferred for type-based dispatch) but invaluable when you need real multi-argument dispatch or value-based dispatch.
+
+**Jerboa gap**: No equivalent. `defmethod` is protocol-style (first-arg struct type). Building multimethods = a dispatch table keyed by the result of a user function, plus an `isa?` relation.
+
+---
+
+## 13. Records and Types
+
+```clojure
+(defrecord Person [name age email])
+
+(def alice (->Person "Alice" 30 "alice@example.com"))
+(def alice2 (map->Person {:name "Alice" :age 30 :email "a@b.com"}))
+
+(:name alice)              ; => "Alice"   (behaves like a map!)
+(assoc alice :age 31)      ; => #Person{...}  still a Person
+(merge alice {:age 31})    ; works, returns Person
+(= alice alice2)           ; structural equality
+```
+
+Records are:
+- **Map-like**: support `get`, `assoc`, `dissoc` (dissoc of a defined field returns a plain map).
+- **Type-stamped**: distinguishable by type for protocol dispatch.
+- **Performant**: fields are real Java fields, not hash lookups.
+- **Protocol-implementing**: can implement protocols inline in the `defrecord` form.
+- **Auto-generate**: constructor (`->Person`), map-constructor (`map->Person`), positional factory.
+
+### `deftype`
+
+Lower level: raw type, doesn't implement `=` by field value, no map interface by default. For when you need "I want to implement a protocol efficiently but don't want map overhead."
+
+### `reify`
+
+Anonymous implementation:
+```clojure
+(reify
+  Runnable
+  (run [_] (println "running"))
+  Shape
+  (area [_] 0))
+```
+
+Creates an unnamed instance implementing those protocols/interfaces. Like an ad-hoc object.
+
+**Jerboa has**: `defrecord` which gives pretty-print and `->alist`. Good, but not protocol-implementing and not the same "record is a map" semantics. Jerboa `defstruct` is more primitive.
+
+---
+
+## 14. Destructuring Everywhere
+
+Possibly the feature Clojure programmers miss most in other languages. Destructuring works in `let`, `fn`, `defn`, `loop`, `doseq`, `for`, `if-let`, `when-let`, `receive`, etc.
+
+### Sequential destructuring
+
+```clojure
+(let [[a b c] [1 2 3]] ...)
+(let [[x & rest] coll] ...)
+(let [[a b :as whole] coll] ...)       ; also binds the whole
+(let [[[a b] c] [[1 2] 3]] ...)        ; nested
+```
+
+### Map destructuring
+
+```clojure
+(let [{:keys [name age]} person] ...)
+;; equivalent to
+(let [name (:name person), age (:age person)] ...)
+
+(let [{name :name, age :age} person] ...)    ; explicit
+(let [{:keys [name age] :or {age 0}} p] ...) ; defaults
+(let [{:keys [name] :as whole} p] ...)
+(let [{:strs [name age]} p] ...)             ; string keys
+(let [{:syms [name age]} p] ...)             ; symbol keys
+(let [{{:keys [street city]} :address} p] ...) ; nested
+```
+
+### In function parameters
+
+```clojure
+(defn greet [{:keys [name age]}]
+  (str "Hello " name ", age " age))
+
+(defn process [config & {:keys [timeout retries] :or {timeout 30 retries 3}}]
+  ...)
+;; call: (process cfg :timeout 60 :retries 5)
+```
+
+Keyword-argument style falls out of rest + map destructuring.
+
+**Jerboa has**: `match` provides strong pattern matching, and `using` gives dot-access to struct fields. But you can't just write `(def (f [x y z]) ...)` and have it destructure a list argument in the parameter position across the language. Destructuring in `let` uses `let-values`/manual `car`/`cdr`. No map destructuring with `:keys` style. Jerboa has `let-alist` which is close.
+
+A destructuring `def`/`let` macro in the prelude would be very high-value.
+
+---
+
+## 15. Metadata
+
+Every Clojure value of a "reference type" (symbols, vars, collections, functions) can carry a metadata map that doesn't affect equality:
+
+```clojure
+(def x ^{:doc "the answer"} 42)                 ; no — numbers can't carry meta
+(def v ^{:flagged true} [1 2 3])                ; collection — yes
+(meta v)                                         ; => {:flagged true}
+(with-meta v {:other :data})                    ; new vector with new meta
+(vary-meta v assoc :count 3)                    ; update meta
+
+(= v [1 2 3])                                    ; true — metadata doesn't affect =
+```
+
+### Shorthand readers
+
+- `^:private` → `^{:private true}`
+- `^String x` → `^{:tag String} x` — type hint for the compiler
+- `^:const` → compile-time inline constant
+- Stackable: `^:private ^String ^{:doc "..."} x`
+
+### Uses
+
+1. **Type hints** for compiler optimization / avoiding reflection.
+2. **Docstrings**, `:file`, `:line`, `:column` — every var carries source location.
+3. **Flags**: `:private`, `:dynamic`, `:deprecated`, `:const`.
+4. **User-defined**: attach arbitrary data to forms for macros to consume.
+
+### `clojure.repl`
+
+Uses metadata heavily: `(doc f)`, `(source f)`, `(dir ns)` — all pulled from var metadata.
+
+**Jerboa gap**: Chez doesn't have a unified metadata concept. You can wrap things but it's not systematic. Big value for introspection, documentation, and macro systems.
+
+---
+
+## 16. Dynamic Vars and Thread-Local Binding
+
+```clojure
+(def ^:dynamic *debug* false)
+
+(defn log [msg]
+  (when *debug*
+    (println "DEBUG:" msg)))
+
+(log "hi")                          ; (nothing)
+(binding [*debug* true]
+  (log "hi"))                       ; DEBUG: hi
+```
+
+`binding` establishes a **thread-local dynamic scope**. Within the binding form (including everything it calls), `*debug*` has the new value. When the scope exits, the old value is restored.
+
+Also thread-aware: `bound-fn`, `bound-fn*` capture current dynamic bindings so you can start a thread that inherits them.
+
+### Classic uses
+
+- `*out*`, `*in*`, `*err*` — stdin/stdout/stderr are dynamic; `with-out-str` redirects.
+- `*print-length*`, `*print-level*` — control REPL output.
+- Database connections, HTTP request context, tracing flags.
+- Anything you'd otherwise pass through 15 function signatures as a context arg.
+
+### `set!` within bindings
+
+Inside a binding thread, dynamic vars can be `set!` to a new value for the rest of the thread's execution. Lets you update state that started as a dynamic default.
+
+**Jerboa gap**: Chez has `fluid-let` and parameters (`make-parameter`, `parameterize`) which are similar. Jerboa's prelude doesn't surface these clearly. A `(def ^:dynamic *foo* ...)` + `(binding ...)` sugar would be natural.
+
+---
+
+## 17. Clojure Spec
+
+Added in Clojure 1.9. Reconsiders data validation, function contracts, and generative testing as one unified system.
+
+```clojure
+(require '[clojure.spec.alpha :as s])
+
+(s/def ::name string?)
+(s/def ::age (s/and integer? #(>= % 0)))
+(s/def ::email (s/and string? #(re-matches #".+@.+" %)))
+
+(s/def ::person (s/keys :req-un [::name ::age] :opt-un [::email]))
+
+(s/valid? ::person {:name "Alice" :age 30})        ; true
+(s/explain ::person {:name "Alice" :age -1})        ; prints readable error
+(s/explain-data ::person {:name "Alice" :age -1})   ; structured error
+(s/conform ::person {:name "Alice" :age 30})        ; canonicalize; :clojure.spec.alpha/invalid on failure
+```
+
+### Spec composition
+
+- `s/and`, `s/or`
+- `s/coll-of`, `s/map-of`, `s/tuple`
+- `s/keys` — maps with required/optional keys, namespaced or unnamespaced
+- `s/cat`, `s/alt`, `s/*`, `s/+`, `s/?` — **regex over sequences** (matches like string regex)
+- `s/nilable`, `s/multi-spec`
+
+### Sequence regex
+
+```clojure
+(s/def ::config (s/cat :name string?
+                        :options (s/* (s/cat :k keyword? :v any?))))
+(s/conform ::config ["foo" :a 1 :b 2])
+;; => {:name "foo" :options [{:k :a :v 1} {:k :b :v 2}]}
+```
+
+### Function specs
+
+```clojure
+(s/fdef transfer
+  :args (s/cat :from ::account :to ::account :amount pos-int?)
+  :ret ::account
+  :fn #(= (-> % :ret :balance)
+          (+ (-> % :args :to :balance) (-> % :args :amount))))
+```
+
+Instruments functions: when enabled, arguments and return values are validated against the spec at call time.
+
+### Generative testing
+
+```clojure
+(s/exercise ::person 5)
+;; => a list of [sample-value conformed] pairs, each a valid person
+```
+
+Every predicate in a spec can have a generator. With `s/fdef`, `clojure.spec.test.alpha/check` runs your function against N generated inputs and asserts the spec holds. Property-based testing nearly for free once you've written specs.
+
+### Why it matters
+
+- **Data validation** with good error messages.
+- **Function contracts** without static typing.
+- **Documentation** — specs are the truth about what shape data takes.
+- **Generative testing** without writing generators.
+
+**Jerboa has**: `list-of?`, `maybe`, `:` for checked cast, `assert!`, but no composable spec system, no function contracts, no generator integration.
+
+This is a large feature. A scaled-down "malli" style (data-driven schema) is the common alternative in Clojure-land today; simpler to port.
+
+---
+
+## 18. Namespaces
+
+Clojure namespaces are first-class runtime objects, not just compile-time file organizers.
+
+```clojure
+(ns myapp.core
+  (:require [clojure.string :as str]
+            [clojure.set :refer [union intersection]]
+            [myapp.db :as db])
+  (:import [java.util Date UUID]))
+```
+
+- `:as` — alias
+- `:refer [x y z]` / `:refer :all` — bring specific names unqualified
+- `:as-alias` — alias without loading (for keyword namespace prefixes)
+- `:rename {old new}`
+- `:import` — for Java classes
+
+### Runtime namespace manipulation
+
+```clojure
+(create-ns 'foo.bar)
+(ns-publics 'clojure.core)     ; map of public names to vars
+(ns-interns 'foo.bar)          ; all interned, including private
+(ns-refers 'foo.bar)
+(intern 'foo.bar 'x 42)        ; programmatically add a var
+(resolve 'some-sym)            ; look up the var
+```
+
+### Hot reload
+
+```clojure
+(require 'myapp.core :reload)
+(require 'myapp.core :reload-all)
+```
+
+Reloads a namespace's file and re-interns its vars. Combined with REPL-driven development, this is the core of the Clojure workflow.
+
+**Jerboa has**: Chez-style module imports with `(import (std ...))`. Less introspection, no hot reload primitives in user code as far as I've seen. `:reload` style dev loop is missing.
+
+---
+
+## 19. EDN and Tagged Literals
+
+**EDN** (Extensible Data Notation) is Clojure's cousin of JSON: a subset of Clojure's reader syntax used as a universal data interchange format. Supports:
+- nil, true, false
+- integers, floats
+- strings, characters
+- symbols, keywords
+- lists, vectors, sets, maps
+- tagged elements `#tag value`
+
+```clojure
+(require '[clojure.edn :as edn])
+(edn/read-string "{:name \"Alice\" :age 30}")
+;; => {:name "Alice", :age 30}
+```
+
+Unlike `read-string` (which can execute code — dangerous on untrusted input), `edn/read-string` is safe.
+
+### Tagged literals
+
+The extension mechanism. `#inst "2026-04-10"` gets parsed by a registered reader fn that returns a `java.util.Date`.
+
+You can register your own:
+```clojure
+(edn/read-string
+  {:readers {'my/point (fn [[x y]] (->Point x y))}}
+  "#my/point [1 2]")
+```
+
+### Data literals are printable
+
+`print-method` dispatch lets records, custom types, etc., round-trip through EDN.
+
+**Jerboa has**: JSON, CSV, YAML support. No EDN-equivalent native "Scheme data serialization" format with tag extensions. S-expressions via `read`/`write` come close, but there's no registered tag extension hook and no safety partition.
+
+---
+
+## 20. Reader Conditionals
+
+For code that runs on multiple platforms (Clojure, ClojureScript, ClojureCLR, babashka):
+
+```clojure
+(defn read-file [path]
+  #?(:clj  (slurp path)
+     :cljs (js/fetch path)
+     :bb   (babashka.fs/read-file path)))
+
+#?@(:clj  [(require '[clojure.java.io :as io])
+            (def base-dir (io/file "."))])
+```
+
+Platform-specific branches baked into the reader. Not #ifdef — actual platform selection at read time.
+
+**Jerboa gap**: Not currently relevant since Jerboa is one target, but reader conditionals become valuable when you have Jerboa-vs-Chez divergences or want to share code with Gerbil.
+
+---
+
+## 21. Rich Number Tower
+
+Clojure inherits and extends the Lisp numeric tower:
+- `Long` (64-bit) by default for integers
+- `Double` for floats
+- **`BigInt`** — arbitrary-precision integer, written `42N`
+- **`BigDecimal`** — arbitrary-precision decimal, written `0.1M`
+- **`Ratio`** — exact rational, automatically from integer division: `(/ 1 3)` → `1/3`
+
+Arithmetic is polymorphic. Integer overflow can either throw (`+`) or promote (`+'`) depending on which operator you use. `*'`, `-'`, `inc'`, `dec'` all auto-promote.
+
+```clojure
+(+ Long/MAX_VALUE 1)        ; ArithmeticException: overflow
+(+' Long/MAX_VALUE 1)       ; 9223372036854775808N   (BigInt)
+(* 1/3 3)                   ; 1N
+(+ 0.1 0.2)                 ; 0.30000000000000004
+(+ 0.1M 0.2M)               ; 0.3M                   (BigDecimal)
+```
+
+Type coercion is also explicit: `int`, `long`, `float`, `double`, `bigint`, `bigdec`, `rationalize`.
+
+**Jerboa/Chez has**: Chez has an excellent number tower — exact rationals, arbitrary precision integers, real and complex numbers, `(sqrt 2)` vs `(exact->inexact (sqrt 2))`. This is one area where **Chez is as good as or better than Clojure**. The gap is stylistic: Clojure's rich number tower is prominent in teaching and everyday use; Chez's is often hidden.
+
+---
+
+## 22. Keyword/Symbol Semantics
+
+Small but constant ergonomic wins:
+
+```clojure
+(:name {:name "Alice"})       ; keywords are functions that look themselves up
+(:age {:name "Alice"} 0)      ; with default
+
+('sym {'sym 42})              ; symbols work too
+```
+
+This one trick makes `(map :name users)` idiomatic where other languages need `(map #(:name %) users)` or equivalent.
+
+### Namespaced keywords
+
+```clojure
+:user/name
+::name                      ; auto-namespaced to current ns
+::db/user                   ; auto-namespaced using :as-alias
+```
+
+Used heavily by spec, clojure.xml, datascript, and API design. Lets you have `:user/name` and `:account/name` without collision.
+
+### Interning and identity
+
+Keywords are interned; `(identical? :foo :foo)` is always true. Same for symbols.
+
+**Jerboa gap**: Jerboa has symbols but no `keyword` distinct type. `name:` reader syntax creates `#:name` keywords — it's there, but less pervasive in API design. Making keywords callable as functions-of-maps in Jerboa would be high impact (requires an applicable-struct system or a wrapper).
+
+---
+
+## 23. `loop`/`recur` and Tail Calls
+
+Clojure runs on the JVM which doesn't guarantee TCO. So Clojure provides **explicit** recur points.
+
+```clojure
+(defn factorial [n]
+  (loop [acc 1, n n]
+    (if (zero? n)
+      acc
+      (recur (* acc n) (dec n)))))
+```
+
+`recur` rebinds the loop bindings and jumps to the top. The compiler enforces it's in tail position; if not, it's a compile error. Same with `recur` inside `fn` — rebinds parameters.
+
+### `trampoline`
+
+For mutual tail recursion:
+```clojure
+(declare odd?)
+(defn even? [n] (if (zero? n) true #(odd? (dec n))))
+(defn odd?  [n] (if (zero? n) false #(even? (dec n))))
+(trampoline even? 1000000)     ; returns thunks to keep stack flat
+```
+
+**Jerboa / Chez**: Chez has proper tail calls — `loop`/`recur` is unnecessary. **This is a Jerboa advantage**. You can write natural recursive code. But adding a `(loop ... (recur ...))` macro for Clojure migrants would still be a nice-to-have for familiarity.
+
+---
+
+## 24. `for` Comprehensions
+
+Not the side-effecting `for` of JS — list comprehensions with filters and lets.
+
+```clojure
+(for [x (range 10)
+      y (range 10)
+      :when (< x y)
+      :let [sum (+ x y)]
+      :while (< sum 15)]
+  [x y sum])
+```
+
+- Multiple generators nest (Cartesian product).
+- `:when pred` — filter.
+- `:let [...]` — intermediate bindings.
+- `:while pred` — break when false (within the innermost loop).
+- Returns a lazy seq.
+
+### `doseq` — side-effect version
+
+```clojure
+(doseq [x (range 5) :when (odd? x)]
+  (println x))
+```
+