# miniKanren-on-SX: relational programming on the CEK/VM miniKanren is not a language to parse — it is an **embedded DSL** implemented as a library of SX functions. No tokenizer, no transpiler. The entire system is a set of `define` forms in `lib/minikanren/`. Programs are SX expressions using the miniKanren API. The unique angle: SX's delimited continuation machinery (`perform`/`cek-resume`, call/cc) maps almost perfectly to the search monad miniKanren needs. Backtracking is cooperative suspension, not a separate trail machine. This is the cleanest possible host for miniKanren. End-state goal: **full core miniKanren** (`run`, `fresh`, `==`, `conde`, `condu`, `onceo`, `project`, `matche`) + **core.logic-style relations** (`appendo`, `membero`, `listo`, `numbero`, etc.) + **arithmetic constraints** (`fd` domain, `CLP(FD)` subset). ## Ground rules - **Scope:** only touch `lib/minikanren/**` and `plans/minikanren-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, or other `lib//`. - **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here. - **SX files:** use `sx-tree` MCP tools only. - **Architecture:** pure library — no source parser. Programs are written in SX using the API. - **Reference:** *The Reasoned Schemer* (Friedman/Byrd/Kiselyov) + Byrd's dissertation. - **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes. ## Architecture sketch ``` SX program using miniKanren API │ ├── lib/minikanren/unify.sx — terms, variables, walk, unification, occurs check ├── lib/minikanren/substitution.sx — substitution as association list / hash table ├── lib/minikanren/stream.sx — lazy streams of substitutions (via delay/force) ├── lib/minikanren/goals.sx — == / fresh / conde / condu / onceo / project / matche ├── lib/minikanren/run.sx — run* / run n — drive the search, extract answers ├── lib/minikanren/relations.sx — standard relations: appendo, membero, listo, etc. └── lib/minikanren/clpfd.sx — arithmetic constraints (CLP(FD) subset) ``` Key semantic mappings: - **Logic variable** → SX vector of length 1 (mutable box); `make-var` creates fresh one; `walk` follows the substitution chain - **Substitution** → SX association list (or hash table for performance) mapping var → term - **Stream of substitutions** → lazy list using `delay`/`force` (Phase 9 of primitives) - **Goal** → SX function `substitution → stream-of-substitutions` - **`==`** → unifies two terms, extending substitution or failing (empty stream) - **`fresh`** → introduces new logic variables; `(fresh (x y) goal)` → goal with x, y bound - **`conde`** → interleave streams from multiple goal clauses (depth-first with interleaving) - **`run n`** → drive the stream, collect first n substitutions, reify answers ## Roadmap ### Phase 1 — variables + unification - [x] `make-var` → fresh logic variable (unique mutable box) - [x] `var?` `v` → bool — is this a logic variable? - [x] `walk` `term` `subst` → follow substitution chain to ground term or unbound var - [x] `walk*` `term` `subst` → deep walk (recurse into lists/dicts) - [x] `unify` `u` `v` `subst` → extended substitution or `#f` (failure) Handles: var/var, var/term, term/var, list unification, number/string/symbol equality. No occurs check by default; `unify-check` with occurs check as opt-in. - [x] Empty substitution `empty-s` (dict-based via kit's `empty-subst` — assoc list was a sketch; kit ships dict, kept it) - [x] Tests in `lib/minikanren/tests/unify.sx`: ground terms, vars, lists, failure, occurs ### Phase 2 — streams + goals - [x] Stream type: `mzero` (empty), `unit s` (singleton), `mk-mplus` (interleave), `mk-bind` (apply goal to stream). Names mk-prefixed because SX has a host `bind` primitive that silently shadows user defines. - [x] Lazy streams via thunks: a paused stream is a zero-arg fn; mk-mplus suspends and swaps when its left operand is paused, giving fair interleaving. - [x] `==` goal: `(fn (s) (let ((s2 (mk-unify u v s))) (if s2 (unit s2) mzero)))` - [x] `==-check` — opt-in occurs-checked equality goal - [x] `succeed` / `fail` — trivial goals - [x] `conj2` / `mk-conj` (variadic) — sequential conjunction - [x] `disj2` / `mk-disj` (variadic) — interleaved disjunction (raw — `conde` adds the implicit-conj-per-clause sugar later) - [x] `fresh` — introduces logic variables inside a goal body. Implemented as a defmacro: `(fresh (x y) g1 g2 ...)` ⇒ `(let ((x (make-var)) (y (make-var))) (mk-conj g1 g2 ...))`. Also `call-fresh` for programmatic goal building. - [x] `conde` — sugar over disj+conj, one row per clause; defmacro that wraps each clause body in `mk-conj` and folds via `mk-disj`. Notes: with eager streams ordering is left-clause-first DFS; true interleaving requires paused thunks (Phase 4 recursive relations). - [x] `condu` — committed choice. defmacro folding clauses into a runtime `condu-try` walker; first clause whose head goal yields a non-empty stream commits its first answer, rest-goals run on that single subst. - [x] `onceo` — `(stream-take 1 (g s))`; trims a goal's stream to ≤1 answer. - [x] Tests: basic goal composition, backtracking, interleaving (110 cumulative) ### Phase 3 — run + reification - [x] `run*` `goal` → list of all answers (reified). defmacro: bind q-name as fresh var, conj goals, take all from stream, reify each. - [x] `run n` `goal` → list of first n answers (defmacro; n = -1 means all) - [x] `reify` `term` `subst` → walk* + build reification subst + walk* again - [x] `reify-s` → maps each unbound var (in left-to-right walk order) to a `_.N` symbol via `(make-symbol (str "_." n))` - [x] `fresh` with multiple variables — already shipped Phase 2B. - [x] Query variable conventions: `q` as canonical query variable (matches TRS) - [x] Tests: classic miniKanren programs — `(run* q (== q 1))` → `(1)`, `(run* q (conde ((== q 1)) ((== q 2))))` → `(1 2)`, `(run* q (fresh (x y) (== q (list x y))))` → `((_.0 _.1))`. Peano + `appendo` deferred to Phase 4. ### Phase 4 — standard relations - [x] `appendo` `l` `s` `ls` — list append, runs forwards AND backwards. Canary green: `(run* q (appendo (1 2) (3 4) q))` → `((1 2 3 4))`; `(run* q (fresh (l s) (appendo l s (1 2 3)) (== q (list l s))))` → all four splits. - [x] `membero` `x` `l` — enumerates: `(run* q (membero q (a b c)))` → `(a b c)` - [x] `listo` `l` — l is a proper list; enumerates list shapes with laziness - [x] `nullo` `l` — l is empty - [x] `pairo` `p` — p is a (non-empty) cons-cell / list - [x] `caro` / `cdro` / `conso` / `firsto` / `resto` - [x] `reverseo` `l` `r` — reverse of list. Forward is fast; backward is `run 1`-clean, `run*` diverges due to interleaved unbounded list search (canonical TRS issue). - [x] `flatteno` `l` `f` — flatten nested lists. Three conde clauses: empty → empty; pair → recurse on car & cdr + appendo; otherwise atom → `(== flat (list tree))`. Atom-ness is asserted via `nafc (nullo tree) + nafc (pairo tree)` — both succeed only when tree is a ground non-list. Works on ground inputs. - [x] `permuteo` `l` `p` — permutation of list. Built on `inserto` (insert at any position) and recursive permutation of tail. All 6 perms of (1 2 3) generated forward; backward `run 1 q (permuteo q (a b c))` finds the input. - [x] `lengtho` `l` `n` — length as a relation, Peano-encoded: `:z` / `(:s :z)` / `(:s (:s :z))` ... matches TRS. Forward is direct; backward enumerates lists of a given length. - [x] Tests: run each relation forwards and backwards (so far 25 in `tests/relations.sx`; reverseo/flatteno/permuteo/lengtho deferred) ### Phase 5 — `project` + `matche` + negation - [x] `project` `(x ...) body` — defmacro: rebinds named vars to `(mk-walk* var s)` in the body's lexical scope, then runs `(mk-conj body...)` on the same substitution. Hygienic via gensym'd `s`-param. (`Phase 5 piece B`) - [x] `matche` — pattern matching over logic terms. Pattern grammar: `_` / symbol / atom / `()` / `(p1 p2 ... pn)`. Each clause becomes `(fresh (vars-in-pat) (== target pat-expr) body...)`. Repeated symbol names in a pattern produce the same fresh var, so they unify (== check). Built without quasiquote (the expander does not recurse into lambda bodies). Fixed-length list patterns only — head/tail destructuring uses `(fresh (a d) (conso a d target) body)` directly. - [x] `conda` — soft-cut: first non-failing head wins; ALL of head's answers flow through rest-goals; later clauses not tried (`Phase 5 piece A`) - [x] `condu` — committed choice (Phase 2) - [x] `nafc` — negation as finite failure: `(nafc g)` yields the input subst iff g has zero answers. Standard caveats apply (open-world unsoundness; diverges if g is infinite). `Phase 5 piece C`. - [ ] Tests: Zebra puzzle, N-queens, Sudoku via `project`, family relations via `matche` ### Phase 6 — arithmetic constraints CLP(FD) - [ ] Finite domain variables: `fd-var` with domain `[lo..hi]` - [x] `ino` `x` `domain` — alias for `(membero x domain)` with the constraint-store-friendly argument order. Sufficient for the enumerate-then-filter style of finite-domain solving. - [x] `all-distincto` `l` — pairwise-distinct elements via `nafc + membero`. - [ ] `fd-eq` `x` `y` — x = y (constraint propagation) - [ ] `fd-neq` `x` `y` — x ≠ y - [ ] `fd-lt` `fd-lte` `fd-gt` `fd-gte` — ordering constraints - [ ] `fd-plus` `x` `y` `z` — x + y = z (constraint) - [ ] `fd-times` `x` `y` `z` — x * y = z - [ ] Arc consistency propagation — when domain narrows, propagate to constrained vars - [ ] Labelling: `fd-run` drives search by splitting domains when propagation stalls - [ ] Tests: send-more-money, N-queens with CLP(FD), map coloring, cryptarithmetic ### Phase 7 — tabling (memoization of relations) - [ ] `tabled` annotation: memoize calls to a relation using a hash table - [ ] Prevents infinite loops in recursive relations like `patho` on cyclic graphs - [ ] Producer/consumer scheduling for tabled relations (variant of SLG resolution) - [ ] Tests: cyclic graph reachability, mutual recursion, Fibonacci via tabling ## Blockers _(none yet)_ ## Progress log _Newest first._ - **2026-05-08** — **numbero / stringo / symbolo (type predicates)**: ground-only type tests via project. Compose with `membero` for type-filtered enumeration: `(fresh (x) (membero x (1 "a" 2 "b" 3)) (numbero x) (== q x))` → `(1 2 3)`. Note: SX keywords are strings, so `(stringo :k)` succeeds. 12 new tests, 328/328 cumulative. - **2026-05-08** — **Graph reachability via patho**: classic miniKanren graph search. `edgeo` looks up edges in a fact list via `membero`; `patho` recursively builds paths via direct-edge OR (one edge + recurse + cons). Enumerates all paths between two nodes, including alternates through shortcuts. 6 new tests, 316/316 cumulative. - **2026-05-08** — **everyo / someo (predicate-style relations)**: `(everyo rel l)` — every element of l satisfies rel; `(someo rel l)` — some element does. Both compose with intarith for numeric tests: `(everyo (fn (x) (lto-i x 10)) (list 1 5 9))` succeeds. 10 new tests, 310/310 cumulative. - **2026-05-08** — **mapo (relational map) — 300 test milestone**: `(mapo rel l1 l2)` succeeds when l2 is l1 with each element rel-related. Works forward and backward; composes with intarith — `(mapo (fn (a b) (*o-i a a b)) (1 2 3 4) q)` → `((1 4 9 16))`. 7 new tests, **300/300** cumulative. - **2026-05-08** — **samelengtho**: classic miniKanren relation that succeeds when two lists have equal length. Symmetric — works to enumerate both lists fresh: `(run 3 q (fresh (l1 l2) (samelengtho l1 l2) (== q (list l1 l2))))` produces empty/empty, then 1-elem pairs, then 2-elem. 5 new tests, 293/293 cumulative. - **2026-05-08** — **Pythagorean triples (intarith showcase)**: search for (a, b, c) ∈ [1..10]³, a ≤ b, a² + b² = c² via `ino + lteo-i + *o-i + pluso-i + ==`. Finds exactly `(3 4 5)` and `(6 8 10)`. Demonstrates the enumerate-then-filter pattern with intarith escape into host arithmetic. 1 new test, 288/288 cumulative. - **2026-05-08** — **intarith.sx — fast ground-only integer arithmetic**: pluso-i / minuso-i / *o-i / lto-i / lteo-i / neqo-i wrap host arithmetic via `project`. They are not relational like Peano `pluso` (require args to walk to numbers), but run at native host speed — a viable escape hatch when puzzle size makes Peano impractical. Composes with relational goals: `(membero x dom) (lto-i x 3)` filters dom by `< 3`. 18 new tests, 287/287 cumulative. - **2026-05-08** — **2x2 Latin square**: small classic constraint demo using `ino` + 4 `all-distincto` constraints. Enumerates exactly 2 squares (`((1 2)(2 1))` and `((2 1)(1 2))`); a clue (top-left = 1) narrows to one. 3 new tests, 269/269 cumulative. Note: 3x3 (12 squares) is the natural showcase but too slow under naive enumerate-then-filter — needs Phase 6 arc-consistency. - **2026-05-08** — **rembero / assoco / nth-o**: more standard list relations. rembero removes the first occurrence (uses `nafc (== a x)` to gate the skip clause, so it's well-defined on ground lists). assoco is alist lookup — works forward (key → value) and backward (value → key). nth-o uses Peano indices into a list. 13 new tests, 266/266 cumulative. - **2026-05-08** — **flatteno**: nested-list flattener via conde + appendo. Atom-ness is detected via `nafc (nullo ...) + nafc (pairo ...)` so the third clause only fires when the input is a ground non-list. Works for `()` / atoms / flat lists / arbitrarily-nested lists. 7 new tests, 253/253 cumulative. Phase 4 list relations all complete. - **2026-05-08** — **Laziness tests**: explicitly verifies the Zzz-on-conde-clauses + mk-mplus-swap-on-paused machinery: an infinitely-recursive relation truncated via `run 4 q (listo-aux q)` produces exactly `(() (_.0) (_.0 _.1) (_.0 _.1 _.2))`; mixing two infinite generators via `mk-disj` keeps both alive (no starvation); `run*` terminates on a bounded query. 3 new tests, 246/246 cumulative. - **2026-05-08** — **N-queens (classic miniKanren benchmark green)**: `lib/minikanren/queens.sx`. Encoding cols(i) = column of queen in row i; `ino-each` + `all-distincto` cover row/column constraints; `safe-diag` uses `project` to escape into host arithmetic for the `|c_i - c_j| ≠ |i - j|` diagonal check; `all-cells-safe` walks pairs at construction time. `(run* q (fresh (a b c d) (== q (list a b c d)) (queens-cols (list a b c d) 4)))` returns the two valid 4-queens placements `(2 4 1 3)` and `(3 1 4 2)`. 6 new tests, 243/243 cumulative. - **2026-05-08** — **Phase 6 piece A — minimal FD (ino + all-distincto)**: `lib/minikanren/fd.sx`. `ino` is `membero` with the FD-style argument order; `all-distincto` is `nafc + membero` walking the list. Together they cover the enumerate-then-filter style of finite-domain solving — `(fresh (a b c) (ino a D) (ino b D) (ino c D) (all-distincto (list a b c)))` enumerates all distinct triples from D. Full FD with arc-consistency, fd-plus etc. is still pending. 9 new tests, 237/237 cumulative. - **2026-05-08** — **Classic puzzles + matche keyword fix**: matche now emits keywords bare in the pattern->expr conversion so they self-evaluate to their string name and unify with the same-keyword target value (instead of becoming a quoted-keyword type). New `tests/classics.sx`: pet permutation puzzle, parent/grandparent inference over a fact list, symbolic differentiation driven by matche dispatch on `:x` / `(:+ a b)` / `(:* a b)` patterns. 6 new tests, 228/228 cumulative. - **2026-05-08** — **Phase 5 piece D — matche, Phase 5 done**: pattern matching macro (`lib/minikanren/matche.sx`) — symbols become fresh vars, atoms become literals, lists recurse positionally, repeated names unify. 14 new tests (literals, vars, wildcards, list patterns, multi-clause dispatch, nested patterns, repeated-var-implies-eq). Built via `cons`/`list` rather than quasiquote because SX's quasiquote does not recurse into lambda bodies — a worth-knowing gotcha. 222/222 cumulative. - **2026-05-08** — **Phase 4 piece C — permuteo + inserto**: standard recursive insert-at-any-position + permute-tail. 7 new tests, including all-6-perms-of-3 as a set check. 208/208 cumulative. - **2026-05-07** — **Phase 5 piece C — nafc**: `lib/minikanren/nafc.sx`. Three-line primitive: stream-take 1; if empty, `(unit s)`, else `mzero`. 7 tests including double-negation and use as a guard. 201/201 cumulative. - **2026-05-07** — **Phase 5 piece B — project**: `lib/minikanren/project.sx` — defmacro that walks each named var, rebinds them, and runs the body's mk-conj. Demonstrated escape into host arithmetic / string ops (`(* n n)`, `(str s "!")`). Hygienic gensym'd s-param. 6 new tests, 194/194 cumulative. - **2026-05-07** — **Peano arithmetic** (`lib/minikanren/peano.sx`): zeroo, pluso, minuso, lteo, lto, *o on Peano-encoded naturals (`:z` / `(:s n)`). pluso runs forward, backward, and enumerates: `(run* q (fresh (a b) (pluso a b 3) (== q (list a b))))` → all 4 pairs summing to 3. *o uses repeated pluso — works for small inputs, slower for larger. 19 new tests, 188/188 cumulative. - **2026-05-07** — **Phase 5 piece A — conda**: soft-cut. Mirrors `condu` minus the `onceo` on the head: all head answers are conjuncted through the rest of the chosen clause. 7 new tests including the conda-vs-condu divergence test. 169/169 cumulative. - **2026-05-07** — **Phase 4 piece B — reverseo + lengtho**: reverseo runs forward cleanly and `run 1`-cleanly backward; lengtho uses Peano-encoded lengths so it works as a true relation in both directions (tests use the encoding directly). 10 new tests, 162/162 cumulative. - **2026-05-07** — **Phase 4 piece A — appendo canary green**: cons-cell support in `unify.sx` + `(:s head tail)` lazy stream refactor in `stream.sx` + hygienic `Zzz` (gensym'd subst-name) wrapping each `conde` clause + `lib/minikanren/ relations.sx` with `nullo` / `pairo` / `caro` / `cdro` / `conso` / `firsto` / `resto` / `listo` / `appendo` / `membero`. 25 new tests in `tests/relations.sx`, 152/152 cumulative. - **Three deep fixes shipped together**, all required to make `appendo` terminate in both directions: 1. SX has no improper pairs, so a stream cell of mature subst + thunk tail can't use `cons` — moved to a `(:s head tail)` tagged shape. 2. `(Zzz g)` wrapped its inner fn in a parameter named `s`, capturing the user goal's own `s` binding (the `(appendo l s ls)` convention). Replaced with `(gensym "zzz-s-")` for hygiene. 3. SX cons cells `(:cons h t)` for relational decomposition (so `(conso a d l)` can split a list by head/tail without proper improper pairs); `mk-walk*` re-flattens cons cells back to native lists for clean reification output. - **2026-05-07** — **Phase 3 done** (run + reification): `lib/minikanren/run.sx` (~28 lines). `reify`/`reify-s`/`reify-name` for canonical `_.N` rendering of unbound vars in left-to-right occurrence order; `run*` / `run` / `run-n` defmacros. 18 new tests in `tests/run.sx`, including the **first classic miniKanren tests green**: `(run* q (== q 1))` → `(1)`; `(run* q (fresh (x y) (== q (list x y))))` → `((_.0 _.1))`. 128/128 cumulative. - **2026-05-07** — **Phase 2 piece D + done** (`condu` / `onceo`): `lib/minikanren/condu.sx`. Both are commitment forms: `onceo` is `(stream-take 1 ...)`; `condu` walks clauses and commits the first one whose head produces an answer. 10 tests in `tests/condu.sx`, 110/110 cumulative. Phase 2 complete — ready for Phase 3 (run + reification). - **2026-05-07** — **Phase 2 piece C** (`conde`): `lib/minikanren/conde.sx` — single defmacro folding clauses through `mk-disj` with internal `mk-conj`. 9 tests in `tests/conde.sx`, 100/100 cumulative. Confirmed eager DFS ordering for ==-only streams; true interleaving is a Phase 4 concern (paused thunks under recursion). - **2026-05-07** — **Phase 2 piece B** (`fresh`): `lib/minikanren/fresh.sx` (~10 lines). defmacro form for nice user-facing syntax + `call-fresh` for programmatic use. 9 new tests in `tests/fresh.sx`, 91/91 cumulative. - **2026-05-07** — **Phase 2 piece A** (streams + ==/conj/disj): `lib/minikanren/stream.sx` (mzero/unit/mk-mplus/mk-bind/stream-take, ~25 lines of code) + `lib/minikanren/goals.sx` (succeed/fail/==/==-check/conj2/disj2/mk-conj/mk-disj, ~30 lines). Found and noted a host-primitive name clash: `bind` is built in and silently shadows user defines — must use `mk-bind`/`mk-mplus` etc. throughout. 34 tests in `tests/goals.sx`, 82/82 cumulative all green. fresh/conde/condu/onceo still pending. - **2026-05-07** — **Phase 1 done**: `lib/minikanren/unify.sx` (53 lines, ~22 lines of actual code) + `lib/minikanren/tests/unify.sx` (48 tests, all green). Kit consumption: `walk-with`, `unify-with`, `occurs-with`, `extend`, `empty-subst`, `mk-var`, `is-var?`, `var-name` all supplied by `lib/guest/match.sx`. Local additions: a miniKanren-flavoured cfg (treats native SX lists as cons-pairs via `:ctor-head = :pair`, occurs-check off), `make-var` fresh-counter, deep `mk-walk*` (kit's `walk*` only recurses into `:ctor` form, not native lists), and `mk-unify` / `mk-unify-check` thin wrappers. The kit earns its keep ~3× over by line count — confirms lib-guest match kit is reusable for logic-language hosts as designed.