conda-try mirrors condu-try but on the chosen clause it (mk-bind (head-goal s) (rest-conj)) — all head answers flow through. condu by contrast applies rest-conj to (first peek), keeping only one head answer. 7 new tests covering: first-non-failing-wins, skip-failing-head, all-fail, no-clauses, the conda-vs-condu divergence (`(1 2)` vs `(1)`), rest-goals running on every head answer, and the soft-cut no-fallthrough property. 169/169 cumulative.
13 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 (deferred — needs atom predicate)permuteolp— permutation of list (deferred to Phase 5 withmatche)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— access reified values of logic vars inside a goal; escapes to ground values for arithmetic or string opsmatche— pattern matching over logic terms (extension from core.logic)(matche l ((head . tail) goal) (() goal))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 with constraint- Tests: Zebra puzzle, N-queens, Sudoku via
project, family relations viamatche
Phase 6 — arithmetic constraints CLP(FD)
- Finite domain variables:
fd-varwith domain[lo..hi] inxdomain— constrain x to domainfd-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)
tabledannotation: memoize calls to a relation using a hash table- Prevents infinite loops in recursive relations like
pathoon cyclic graphs - Producer/consumer scheduling for tabled relations (variant of SLG resolution)
- Tests: cyclic graph reachability, mutual recursion, Fibonacci via tabling
Blockers
(none yet)
Progress log
Newest first.
- 2026-05-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.