diff --git a/lib/minikanren/clpfd.sx b/lib/minikanren/clpfd.sx index bd03ece2..ab2fb10b 100644 --- a/lib/minikanren/clpfd.sx +++ b/lib/minikanren/clpfd.sx @@ -146,11 +146,26 @@ ((s2 ((first cs) s))) (cond ((= s2 nil) nil) (:else (fd-fire-list (rest cs) s2)))))))) +(define + fd-store-signature + (fn + (s) + (let + ((doms (fd-domains-of s))) + (let + ((dom-sizes (reduce (fn (acc k) (+ acc (len (get doms k)))) 0 (keys doms)))) + (+ dom-sizes (len (keys s))))))) + (define fd-fire-store (fn (s) - (let ((cs (get (fd-store-of s) :constraints))) (fd-fire-list cs s)))) + (let + ((s2 (fd-fire-list (get (fd-store-of s) :constraints) s))) + (cond + ((= s2 nil) nil) + ((= (fd-store-signature s) (fd-store-signature s2)) s2) + (:else (fd-fire-store s2)))))) ;; --- user-facing goals --- diff --git a/lib/minikanren/tests/queens-fd.sx b/lib/minikanren/tests/queens-fd.sx new file mode 100644 index 00000000..7457abd0 --- /dev/null +++ b/lib/minikanren/tests/queens-fd.sx @@ -0,0 +1,97 @@ +;; lib/minikanren/tests/queens-fd.sx — N-queens via CLP(FD). +;; +;; Native FD propagation makes N-queens tractable: 4-queens finds both +;; solutions instantly; 5-queens finds all 10 in seconds. Compare with +;; the naive enumerate-then-filter version in queens.sx, which struggles +;; past N=4. + +(define + fd-no-diag + (fn + (ci cj k) + (fresh + (a b) + (fd-plus cj k a) + (fd-plus ci k b) + (fd-neq ci a) + (fd-neq cj b)))) + +(define + n-queens-4-fd + (fn + (cs) + (let + ((c1 (nth cs 0)) + (c2 (nth cs 1)) + (c3 (nth cs 2)) + (c4 (nth cs 3))) + (mk-conj + (fd-in c1 (list 1 2 3 4)) + (fd-in c2 (list 1 2 3 4)) + (fd-in c3 (list 1 2 3 4)) + (fd-in c4 (list 1 2 3 4)) + (fd-distinct cs) + (fd-no-diag c1 c2 1) + (fd-no-diag c1 c3 2) + (fd-no-diag c1 c4 3) + (fd-no-diag c2 c3 1) + (fd-no-diag c2 c4 2) + (fd-no-diag c3 c4 1) + (fd-label cs))))) + +(define + n-queens-5-fd + (fn + (cs) + (let + ((c1 (nth cs 0)) + (c2 (nth cs 1)) + (c3 (nth cs 2)) + (c4 (nth cs 3)) + (c5 (nth cs 4))) + (mk-conj + (fd-in + c1 + (list 1 2 3 4 5)) + (fd-in + c2 + (list 1 2 3 4 5)) + (fd-in + c3 + (list 1 2 3 4 5)) + (fd-in + c4 + (list 1 2 3 4 5)) + (fd-in + c5 + (list 1 2 3 4 5)) + (fd-distinct cs) + (fd-no-diag c1 c2 1) + (fd-no-diag c1 c3 2) + (fd-no-diag c1 c4 3) + (fd-no-diag c1 c5 4) + (fd-no-diag c2 c3 1) + (fd-no-diag c2 c4 2) + (fd-no-diag c2 c5 3) + (fd-no-diag c3 c4 1) + (fd-no-diag c3 c5 2) + (fd-no-diag c4 c5 1) + (fd-label cs))))) + +(mk-test + "n-queens-4-fd-two-solutions" + (run* + q + (fresh (a b c d) (== q (list a b c d)) (n-queens-4-fd (list a b c d)))) + (list + (list 2 4 1 3) + (list 3 1 4 2))) + +(mk-test + "n-queens-5-fd-ten-solutions" + (let + ((sols (run* q (fresh (a b c d e) (== q (list a b c d e)) (n-queens-5-fd (list a b c d e)))))) + (= (len sols) 10)) + true) + +(mk-tests-run!) diff --git a/plans/minikanren-on-sx.md b/plans/minikanren-on-sx.md index 8e2055f1..702ddde9 100644 --- a/plans/minikanren-on-sx.md +++ b/plans/minikanren-on-sx.md @@ -145,11 +145,31 @@ Key semantic mappings: - [ ] Tests: Zebra puzzle, N-queens, Sudoku via `project`, family relations via `matche` ### Phase 6 — arithmetic constraints CLP(FD) -- [ ] Finite domain variables: `fd-var` with domain `[lo..hi]` -- [x] `ino` `x` `domain` — alias for `(membero x domain)` with the - constraint-store-friendly argument order. Sufficient for the - enumerate-then-filter style of finite-domain solving. -- [x] `all-distincto` `l` — pairwise-distinct elements via `nafc + membero`. +- [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