Files
rose-ash/plans/minikanren-on-sx.md
giles 57a84b372d
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 57s
Merge loops/minikanren into architecture: full miniKanren-on-SX library
Squash merge of 76 commits from loops/minikanren. Adds lib/minikanren/
— a complete miniKanren-on-SX implementation built on top of
lib/guest/match.sx, validating the lib-guest unify-and-match kit as
intended.

Modules (20 .sx files, ~1700 LOC):
  unify, stream, goals, fresh, conde, condu, conda, run, relations,
  peano, intarith, project, nafc, matche, fd, queens, defrel, clpfd,
  tabling

Phases 1–5 fully done (core miniKanren API, all classic relations,
matche, conda, project, nafc).

Phase 6 — native CLP(FD): domain primitives, fd-in / fd-eq / fd-neq /
fd-lt / fd-lte / fd-plus / fd-times / fd-distinct / fd-label, with
constraint reactivation iterating to fixed point. N-queens via FD:
4-queens 2 solutions, 5-queens 10 solutions (vs naive timeout past N=4).

Phase 7 — naive ground-arg tabling: table-1 / table-2 / table-3.
Fibonacci canary: tab-fib(25) = 75025 in seconds, naive fib(25) times
out at 60s. Ackermann via table-3: A(3,3) = 61.

71 test files, 644+ tests passing across the suite. Producer/consumer
SLG (cyclic patho, mutual recursion) deferred — research-grade work.

The lib-guest validation experiment is conclusive: lib/minikanren/
unify.sx adds ~50 lines of local logic (custom cfg, deep walk*, fresh
counter) over lib/guest/match.sx's ~100-line kit. The kit earns its
keep ~3× by line count.
2026-05-08 23:01:54 +00:00

447 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<lang>/`.
- **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)
- [x] Finite domain variables: domain stored under reserved key `_fd` in
the substitution dict; ascending sorted-int-list representation;
domain primitives `fd-dom-from-list`, `fd-dom-intersect`,
`fd-dom-without`, `fd-dom-range`, `fd-dom-min/max/empty?/singleton?`.
- [x] `fd-in x dom` — narrows x's domain by intersection.
- [x] `fd-eq x y`, `fd-neq x y`, `fd-lt`, `fd-lte` — propagator-store
goals. Each adds a closure to the constraints field and runs it
on post; closures re-fire after every label step via fd-fire-store.
- [x] `fd-plus x y z`, `fd-times x y z` — ground-cases propagators
(when 2 of 3 walk to numbers, the third is derived).
- [x] `fd-distinct vars` — pairwise alldifferent via fd-neq folds.
- [x] Constraint reactivation: `fd-fire-store` iterates to fixed point
using a domain+bindings signature comparison; ensures multi-step
propagation chains (e.g. fd-plus binds a fresh var, which then
lets a downstream fd-neq fire).
- [x] Labelling: `fd-label vars` enumerates each var's domain via
mk-mplus over singleton bindings; constraint store re-fires after
each binding.
- [x] Tests: N-queens via FD — 4-queens finds both solutions, 5-queens
finds all 10 in seconds (vs the naive enumerate-then-filter
version which times out past N=4).
- [x] `ino` `x` `domain` — alias for `(membero x domain)` (kept for
the simple enumerate-then-filter pattern alongside fd-in).
- [x] `all-distincto` `l` — original membero-based version (kept alongside
the newer fd-distinct).
- [ ] `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)
- [x] `table-1`, `table-2`, `table-3` wrappers: ground-arg memoization
for 1-, 2-, and 3-argument relations.
- [x] `table-2` wrapper: ground-arg memoization for 2-arg relations.
Cache keyed by walked input; on miss runs underlying relation,
collects all output values from the answer stream, stores, and
replays. Subsequent calls with the same ground input replay the
cached values (no recomputation).
- [x] Fibonacci canary green: tabled `fib(25) = 75025` in seconds;
naive `fib(25)` times out at 60s. Memoization turns exponential
recursion into linear.
- [x] Ackermann canary green via `table-3`: `A(3, 3) = 61`.
- [ ] Producer/consumer SLG scheduling — required to handle recursive
tabled calls with the SAME ground key (e.g. cyclic `patho` with a
shared key); naive memoization deferred to a future iteration.
- [ ] Tests: cyclic graph reachability via tabled patho (deferred —
requires SLG); mutual recursion (deferred).
## Blockers
_(none yet)_
## Progress log
_Newest first._
- **2026-05-08** — **Session snapshot**: 17 lib files, 61 test files, 1229
library LOC + 4360 test LOC, **551/551 tests cumulative**. Library covers
Phases 15 fully, Phase 6 partial (FD helpers + intarith escape), Phase 7
documented via cyclic-graph divergence test. lib-guest validation
completed: `lib/minikanren/unify.sx` ≈ 50 LOC of local logic over
`lib/guest/match.sx`'s ≈100 LOC kit (kit earns ~3× by line count). Major
classic miniKanren tests green: appendo forwards/backwards, Peano
arithmetic, 4-queens, Pythagorean triples, family-relations / pet
puzzle / symbolic differentiation, 2x2 Latin square. Ready for Phase 6
(native FD with arc-consistency) and Phase 7 (tabling) as future work.
- **2026-05-08** — **zip-with-o**: element-wise relational combine over two
lists with a 3-arg combiner. Ground-only by composition. 5 new tests.
- **2026-05-08** — **take-while-o + drop-while-o**: predicate-driven
prefix/suffix split. Roundtrip property verified. 8 new tests.
- **2026-05-08** — **arith-progo**: arithmetic-progression list generator
via project. 6 new tests.
- **2026-05-08** — **counto**: count occurrences of x in l (intarith).
6 new tests.
- **2026-05-08** — **nub-o**: dedupe via membero-on-tail. Multiplicity
caveat documented in tests. 5 new tests.
- **2026-05-08** — **simplify-step-o**: algebraic identity simplifier
(conda demo). 6 new tests.
- **2026-05-08** — **flat-mapo**: concatMap-style relation. 5 new tests.
- **2026-05-08** — **foldl-o (relational left fold)**: complement to foldr-o. Combiner has args (acc, head) -> new-acc. (foldl-o pluso-i (1 2 3 4 5) 0 q) -> 15; (foldl-o flipped-conso l () q) reverses l. 5 new tests, 510/510 cumulative.
- **2026-05-08** — **foldr-o (relational right fold)**: takes a 3-arg combiner relation rel, a list, an initial accumulator, produces the result. (foldr-o appendo lists () q) is a flatten; (foldr-o conso l () q) rebuilds l. 4 new tests, 505/505 cumulative.
- **2026-05-08** — **enumerate-i / enumerate-from-i — 500-test milestone**: index-each-element relations. (enumerate-i l result) -> result is l with each element paired with its 0-based index. (enumerate-from-i n l result) starts at n. 5 new tests, **501/501** cumulative.
- **2026-05-08** — **partitiono**: relational partition. (partitiono pred l yes no) splits l so yes contains elements where pred succeeds and no contains the rest. Composes with intarith for numeric predicates. 5 new tests, 496/496 cumulative.
- **2026-05-08** — **appendo3**: 3-list append via two appendos. (appendo3 a b c r) is (appendo a b mid) ∧ (appendo mid c r). Recovers any of the three lists given the other two and the result. 5 new tests, 491/491 cumulative.
- **2026-05-08** — **lengtho-i**: integer-indexed length (intarith). Empty list -> 0; recurse with pluso-i. Drop-in fast replacement for Peano lengtho when the count fits in a host integer. 5 new tests, 486/486 cumulative.
- **2026-05-08** — **sumo + producto (intarith)**: fold a list of ground integers to its sum or product. Empty list -> identity (0 / 1). Recurse via pluso-i / *o-i. 9 new tests, 481/481 cumulative.
- **2026-05-08** — **mino + maxo (intarith)**: find the minimum/maximum of a non-empty list of ground integers. Two conde clauses each: singleton (return the element) / multi (compare head against recursive min/max of tail). 9 new tests, 472/472 cumulative.
- **2026-05-08** — **sortedo (intarith)**: list is non-decreasing under integer ordering. Three conde clauses: empty / singleton / pair-and-recurse. Uses lteo-i for the adjacent-pair check (ground integers). 7 new tests, 463/463 cumulative.
- **2026-05-08** — **subseto**: every element of l1 is a member of l2. Recurses on l1, checking each element via membero. Allows duplicates in l1 (each independently in l2). 7 new tests, 456/456 cumulative.
- **2026-05-08** — **cycle-free path search**: mitigation for the cyclic-graph divergence — thread a accumulator through the recursion, gate each step with nafc + membero on visited. Terminates on graphs with cycles; no Phase-7 tabling required for the simple case. 3 new tests, 449/449 cumulative.
- **2026-05-08** — **removeo-allo**: removes every occurrence of x (vs rembero, which removes only the first). Three conde clauses: empty -> empty; head matches -> skip and recurse; head differs (nafc) -> keep and recurse. 5 new tests, 446/446 cumulative.
- **2026-05-08** — **btree-walko (matche showcase)**: walks a binary tree (:leaf v) | (:node l r) and emits each leaf value via conde. Demonstrates matche dispatch on tagged-list patterns, recursion through both branches via conde, and run* enumerating all 5 leaves of a small tree. 4 new tests, 441/441 cumulative.
- **2026-05-08** — **swap-firsto**: swap the first two elements of a list. Built via four conso constraints. Self-inverse on length-2+ lists; runs forward and backward. 6 new tests, 437/437 cumulative.
- **2026-05-08** — **pairlisto**: relational zip — pairs is the zipped list of (l1[i] l2[i]). Runs forward, recovers l1 given l2 and pairs, recovers l2 given l1 and pairs. 5 new tests, 431/431 cumulative.
- **2026-05-08** — **iterate-no**: relational iterated application. Applies a 2-arg relation n times (Peano n) starting from x to produce result. Generalises succ-iteration / list-cons-iteration / etc. 4 new tests, 426/426 cumulative.
- **2026-05-08** — **rev-acco / rev-2o**: accumulator-style reversal — faster than appendo-driven reverseo for forward queries because no quadratic appendos. Trade-off: rev-acco is asymmetric (cannot run cleanly backward in run*). 6 new tests, 422/422 cumulative.
- **2026-05-08** — **even-i / odd-i (intarith)**: ground-only parity goals via project. Composes with membero for filtered enumeration: -> . 5 new tests, 416/416 cumulative.
- **2026-05-08** — **selecto**: classic miniKanren "choose an element + rest". Direct base (l = (x . rest)) plus skip-head recurse. Enumerates all (element, rest) splits in run*; runs forward, backward, mid-pipeline. 6 new tests, 411/411 cumulative.
- **2026-05-08** — **subo (contiguous sublist)**: Two appendos chained — l = front ++ s ++ back. Goal order matters: appendo on the ground l first, so the search is finitary; then constrain front. 7 new tests, 405/405 cumulative.
- **2026-05-08** — **prefixo + suffixo**: classic appendo-derived sublist relations. (prefixo p l) ≡ p ⊕ ? = l; (suffixo s l) ≡ ? ⊕ s = l. Both enumerate all prefixes/suffixes when given a fresh first arg. 9 new tests, 398/398 cumulative.
- **2026-05-08** — **palindromeo**: 2-line definition (reverseo + ==). Succeeds when a list reads the same forwards and backwards. 7 new tests, 389/389 cumulative.
- **2026-05-08** — **not-membero**: relational "x is not a member of l".
Uses `nafc + ==` per element (the same skeleton all-distincto uses).
Useful as a constraint-style filter: `(membero x dom) (not-membero x
excluded)`. 4 new tests, 382/382 cumulative.
- **2026-05-08** — **repeato + concato**: repeato builds a list of n copies
(Peano n); concato is fold-appendo over a list of lists. Both run forward
and backward — repeato can recover the count from a uniform list. 10 new
tests, 378/378 cumulative.
- **2026-05-08** — **tako + dropo (Peano-indexed prefix/suffix)**: takes / drops
the first n elements via a Peano-encoded count. Round-trip
`(tako n l) ⊕ (dropo n l) = l` holds. 9 new tests, 368/368 cumulative.
- **2026-05-08** — **eveno / oddo Peano predicates**: classic miniKanren parity
relations. eveno: 0 or successor-of-successor of even; oddo: 1 or
successor-of-successor of odd. Both run forward (test) and backward
(enumerate). 12 new tests, 359/359 cumulative.
- **2026-05-08** — **defrel — Prolog-style relation definition macro**:
`(defrel (NAME ARGS...) (CLAUSE1 ...) (CLAUSE2 ...) ...)` expands to
`(define NAME (fn (ARGS...) (conde (CLAUSE1 ...) (CLAUSE2 ...) ...)))`.
Mirrors Prolog's clause syntax and inherits Zzz-via-conde so recursive
relations terminate. 3 new tests, 347/347 cumulative.
- **2026-05-08** — **lasto / init-o**: classic head/tail-style list relations.
lasto extracts the final element; init-o extracts everything-but-the-last.
Together with appendo, the round-trip `init ⊕ (last) = l` holds. 8 new
tests, 344/344 cumulative.
- **2026-05-08** — **Datalog-style relational database queries**: tests/rdb.sx
shows miniKanren as a query engine over fact tables. Defines two tables
(employees + project assignments), wraps each with a relation that does
membero over the table, then expresses queries: dept filter, salary >
threshold (intarith), join engineers ↔ runtime project, find anyone on
multiple distinct projects (nafc + ==). 5 new tests, 336/336 cumulative.
- **2026-05-08** — **Cyclic graph behaviour test (motivates Phase 7 tabling)**:
Demonstrates that naive patho on a 2-cycle generates ever-longer paths.
`run 5` truncates to a finite prefix; `run*` would diverge. This is
exactly the test case Phase 7 (tabled / SLG resolution) is designed
to fix. 3 new tests, 331/331 cumulative.
- **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.