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

32 KiB
Raw Blame History

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

  • make-var → fresh logic variable (unique mutable box)
  • var? v → bool — is this a logic variable?
  • walk term subst → follow substitution chain to ground term or unbound var
  • walk* term subst → deep walk (recurse into lists/dicts)
  • 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.
  • Empty substitution empty-s (dict-based via kit's empty-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 host bind primitive 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 goal
  • succeed / fail — trivial goals
  • conj2 / mk-conj (variadic) — sequential conjunction
  • disj2 / mk-disj (variadic) — interleaved disjunction (raw — conde adds 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 ...)). Also call-fresh for programmatic goal building.
  • 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).
  • 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.
  • 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 n goal → list of first n answers (defmacro; n = -1 means all)
  • reify term subst → walk* + build reification subst + walk* again
  • reify-s → maps each unbound var (in left-to-right walk order) to a _.N symbol via (make-symbol (str "_." n))
  • fresh with multiple variables — already shipped Phase 2B.
  • Query variable conventions: q as 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 + appendo deferred to Phase 4.

Phase 4 — standard relations

  • 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.
  • membero x l — enumerates: (run* q (membero q (a b c)))(a b c)
  • listo l — l is a proper list; enumerates list shapes with laziness
  • nullo l — l is empty
  • pairo p — p is a (non-empty) cons-cell / list
  • caro / cdro / conso / firsto / resto
  • 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).
  • 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.
  • 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.
  • 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.
  • 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'd s-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 via matche

Phase 6 — arithmetic constraints CLP(FD)

  • 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?.
  • 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-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).
  • Labelling: fd-label vars enumerates 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).
  • ino x domain — alias for (membero x domain) (kept for the simple enumerate-then-filter pattern alongside fd-in).
  • 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)

  • table-1, table-2, table-3 wrappers: ground-arg memoization for 1-, 2-, and 3-argument relations.
  • 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).
  • Fibonacci canary green: tabled fib(25) = 75025 in seconds; naive fib(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 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-08Session 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-08zip-with-o: element-wise relational combine over two lists with a 3-arg combiner. Ground-only by composition. 5 new tests.
  • 2026-05-08take-while-o + drop-while-o: predicate-driven prefix/suffix split. Roundtrip property verified. 8 new tests.
  • 2026-05-08arith-progo: arithmetic-progression list generator via project. 6 new tests.
  • 2026-05-08counto: count occurrences of x in l (intarith). 6 new tests.
  • 2026-05-08nub-o: dedupe via membero-on-tail. Multiplicity caveat documented in tests. 5 new tests.
  • 2026-05-08simplify-step-o: algebraic identity simplifier (conda demo). 6 new tests.
  • 2026-05-08flat-mapo: concatMap-style relation. 5 new tests.
  • 2026-05-08foldl-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-08foldr-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-08enumerate-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-08partitiono: 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-08appendo3: 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-08lengtho-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-08sumo + 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-08mino + 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-08sortedo (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-08subseto: 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-08cycle-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-08removeo-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-08btree-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-08swap-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-08pairlisto: 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-08iterate-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-08rev-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-08even-i / odd-i (intarith): ground-only parity goals via project. Composes with membero for filtered enumeration: -> . 5 new tests, 416/416 cumulative.
  • 2026-05-08selecto: 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-08subo (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-08prefixo + 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-08palindromeo: 2-line definition (reverseo + ==). Succeeds when a list reads the same forwards and backwards. 7 new tests, 389/389 cumulative.
  • 2026-05-08not-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-08repeato + 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-08tako + 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-08eveno / 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-08defrel — 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-08lasto / 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-08Datalog-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-08Cyclic 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-08numbero / 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-08Graph 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-08everyo / 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-08mapo (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-08samelengtho: 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-08Pythagorean 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-08intarith.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-082x2 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-08rembero / 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-08flatteno: 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-08Laziness 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-08N-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-08Phase 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-08Classic 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-08Phase 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-08Phase 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-07Phase 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-07Phase 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-07Peano 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-07Phase 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-07Phase 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-07Phase 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-07Phase 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-07Phase 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-07Phase 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-07Phase 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-07Phase 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-07Phase 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.