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.
32 KiB
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/**andplans/minikanren-on-sx.md. Do not editspec/,hosts/,shared/, or otherlib/<lang>/. - Shared-file issues go under "Blockers" below with a minimal repro; do not fix here.
- SX files: use
sx-treeMCP 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 logupdated 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-varcreates fresh one;walkfollows 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 boundconde→ 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
make-var→ fresh logic variable (unique mutable box)var?v→ bool — is this a logic variable?walktermsubst→ follow substitution chain to ground term or unbound varwalk*termsubst→ deep walk (recurse into lists/dicts)unifyuvsubst→ extended substitution or#f(failure) Handles: var/var, var/term, term/var, list unification, number/string/symbol equality. No occurs check by default;unify-checkwith occurs check as opt-in.- Empty substitution
empty-s(dict-based via kit'sempty-subst— assoc list was a sketch; kit ships dict, kept it) - Tests in
lib/minikanren/tests/unify.sx: ground terms, vars, lists, failure, occurs
Phase 2 — streams + goals
- Stream type:
mzero(empty),unit s(singleton),mk-mplus(interleave),mk-bind(apply goal to stream). Names mk-prefixed because SX has a hostbindprimitive that silently shadows user defines. - 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.
==goal:(fn (s) (let ((s2 (mk-unify u v s))) (if s2 (unit s2) mzero)))==-check— opt-in occurs-checked equality goalsucceed/fail— trivial goalsconj2/mk-conj(variadic) — sequential conjunctiondisj2/mk-disj(variadic) — interleaved disjunction (raw —condeadds the implicit-conj-per-clause sugar later)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 ...)). Alsocall-freshfor programmatic goal building.conde— sugar over disj+conj, one row per clause; defmacro that wraps each clause body inmk-conjand folds viamk-disj. Notes: with eager streams ordering is left-clause-first DFS; true interleaving requires paused thunks (Phase 4 recursive relations).condu— committed choice. defmacro folding clauses into a runtimecondu-trywalker; first clause whose head goal yields a non-empty stream commits its first answer, rest-goals run on that single subst.onceo—(stream-take 1 (g s)); trims a goal's stream to ≤1 answer.- Tests: basic goal composition, backtracking, interleaving (110 cumulative)
Phase 3 — run + reification
run*goal→ list of all answers (reified). defmacro: bind q-name as fresh var, conj goals, take all from stream, reify each.run ngoal→ list of first n answers (defmacro; n = -1 means all)reifytermsubst→ walk* + build reification subst + walk* againreify-s→ maps each unbound var (in left-to-right walk order) to a_.Nsymbol via(make-symbol (str "_." n))freshwith multiple variables — already shipped Phase 2B.- Query variable conventions:
qas canonical query variable (matches TRS) - 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 +appendodeferred to Phase 4.
Phase 4 — standard relations
appendolsls— 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.memberoxl— enumerates:(run* q (membero q (a b c)))→(a b c)listol— l is a proper list; enumerates list shapes with lazinessnullol— l is emptypairop— p is a (non-empty) cons-cell / listcaro/cdro/conso/firsto/restoreverseolr— reverse of list. Forward is fast; backward isrun 1-clean,run*diverges due to interleaved unbounded list search (canonical TRS issue).flattenolf— flatten nested lists. Three conde clauses: empty → empty; pair → recurse on car & cdr + appendo; otherwise atom →(== flat (list tree)). Atom-ness is asserted vianafc (nullo tree) + nafc (pairo tree)— both succeed only when tree is a ground non-list. Works on ground inputs.permuteolp— permutation of list. Built oninserto(insert at any position) and recursive permutation of tail. All 6 perms of (1 2 3) generated forward; backwardrun 1 q (permuteo q (a b c))finds the input.lengtholn— length as a relation, Peano-encoded::z/(:s :z)/(:s (:s :z))... matches TRS. Forward is direct; backward enumerates lists of a given length.- Tests: run each relation forwards and backwards (so far 25 in
tests/relations.sx; reverseo/flatteno/permuteo/lengtho deferred)
Phase 5 — project + matche + negation
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'ds-param. (Phase 5 piece B)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.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)condu— committed choice (Phase 2)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 viamatche
Phase 6 — arithmetic constraints CLP(FD)
- Finite domain variables: domain stored under reserved key
_fdin the substitution dict; ascending sorted-int-list representation; domain primitivesfd-dom-from-list,fd-dom-intersect,fd-dom-without,fd-dom-range,fd-dom-min/max/empty?/singleton?. fd-in x dom— narrows x's domain by intersection.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.fd-plus x y z,fd-times x y z— ground-cases propagators (when 2 of 3 walk to numbers, the third is derived).fd-distinct vars— pairwise alldifferent via fd-neq folds.- Constraint reactivation:
fd-fire-storeiterates 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). - Labelling:
fd-label varsenumerates each var's domain via mk-mplus over singleton bindings; constraint store re-fires after each binding. - 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).
inoxdomain— alias for(membero x domain)(kept for the simple enumerate-then-filter pattern alongside fd-in).all-distinctol— original membero-based version (kept alongside the newer fd-distinct).fd-eqxy— x = y (constraint propagation)fd-neqxy— x ≠ yfd-ltfd-ltefd-gtfd-gte— ordering constraintsfd-plusxyz— x + y = z (constraint)fd-timesxyz— x * y = z- Arc consistency propagation — when domain narrows, propagate to constrained vars
- Labelling:
fd-rundrives 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)
table-1,table-2,table-3wrappers: ground-arg memoization for 1-, 2-, and 3-argument relations.table-2wrapper: 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).- Fibonacci canary green: tabled
fib(25) = 75025in seconds; naivefib(25)times out at 60s. Memoization turns exponential recursion into linear. - 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
pathowith 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 1–5 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 overlib/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) = lholds. 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) = lholds. 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 5truncates 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
memberofor 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.
edgeolooks up edges in a fact list viamembero;pathorecursively 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 Peanopluso(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+ 4all-distinctoconstraints. 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 viamk-disjkeeps 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-distinctocover row/column constraints;safe-diagusesprojectto escape into host arithmetic for the|c_i - c_j| ≠ |i - j|diagonal check;all-cells-safewalks 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.inoismemberowith the FD-style argument order;all-distinctoisnafc + memberowalking 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 viacons/listrather 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), elsemzero. 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
conduminus theonceoon 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 instream.sx+ hygienicZzz(gensym'd subst-name) wrapping eachcondeclause +lib/minikanren/ relations.sxwithnullo/pairo/caro/cdro/conso/firsto/resto/listo/appendo/membero. 25 new tests intests/relations.sx, 152/152 cumulative.- Three deep fixes shipped together, all required to make
appendoterminate in both directions:- 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. (Zzz g)wrapped its inner fn in a parameter nameds, capturing the user goal's ownsbinding (the(appendo l s ls)convention). Replaced with(gensym "zzz-s-")for hygiene.- 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.
- SX has no improper pairs, so a stream cell of mature subst + thunk
tail can't use
- Three deep fixes shipped together, all required to make
- 2026-05-07 — Phase 3 done (run + reification):
lib/minikanren/run.sx(~28 lines).reify/reify-s/reify-namefor canonical_.Nrendering of unbound vars in left-to-right occurrence order;run*/run/run-ndefmacros. 18 new tests intests/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:onceois(stream-take 1 ...);conduwalks clauses and commits the first one whose head produces an answer. 10 tests intests/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 throughmk-disjwith internalmk-conj. 9 tests intests/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-freshfor programmatic use. 9 new tests intests/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:bindis built in and silently shadows user defines — must usemk-bind/mk-mplusetc. throughout. 34 tests intests/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-nameall supplied bylib/guest/match.sx. Local additions: a miniKanren-flavoured cfg (treats native SX lists as cons-pairs via:ctor-head = :pair, occurs-check off),make-varfresh-counter, deepmk-walk*(kit'swalk*only recurses into:ctorform, not native lists), andmk-unify/mk-unify-checkthin wrappers. The kit earns its keep ~3× over by line count — confirms lib-guest match kit is reusable for logic-language hosts as designed.