diff --git a/lib/minikanren/nafc.sx b/lib/minikanren/nafc.sx new file mode 100644 index 00000000..181c84ac --- /dev/null +++ b/lib/minikanren/nafc.sx @@ -0,0 +1,24 @@ +;; lib/minikanren/nafc.sx — Phase 5 piece C: negation as finite failure. +;; +;; (nafc g) +;; succeeds (yields the input substitution) if g has zero answers +;; against that substitution; fails (mzero) if g has at least one. +;; +;; Caveat: `nafc` is unsound under the open-world assumption. It only +;; makes sense for goals over fully-ground terms, or with the explicit +;; understanding that adding more facts could flip the answer. Use +;; `(project (...) ...)` to ensure the relevant vars are ground first. +;; +;; Caveat 2: stream-take forces g for at least one answer; if g is +;; infinitely-ground (say, a divergent search over an unbound list), +;; nafc itself will diverge. Standard miniKanren limitation. + +(define + nafc + (fn + (g) + (fn + (s) + (let + ((peek (stream-take 1 (g s)))) + (if (empty? peek) (unit s) mzero))))) diff --git a/lib/minikanren/tests/nafc.sx b/lib/minikanren/tests/nafc.sx new file mode 100644 index 00000000..cec8eaa6 --- /dev/null +++ b/lib/minikanren/tests/nafc.sx @@ -0,0 +1,50 @@ +;; lib/minikanren/tests/nafc.sx — Phase 5 piece C tests for `nafc`. + +(mk-test + "nafc-failed-goal-succeeds" + (run* q (nafc (== 1 2))) + (list (make-symbol "_.0"))) + +(mk-test + "nafc-successful-goal-fails" + (run* q (nafc (== 1 1))) + (list)) + +(mk-test + "nafc-double-negation" + (run* q (nafc (nafc (== 1 1)))) + (list (make-symbol "_.0"))) + +(mk-test + "nafc-with-conde-no-clauses-succeed" + (run* + q + (nafc + (conde ((== 1 2)) ((== 3 4))))) + (list (make-symbol "_.0"))) + +(mk-test + "nafc-with-conde-some-clause-succeeds-fails" + (run* + q + (nafc + (conde ((== 1 1)) ((== 3 4))))) + (list)) + +;; --- composing nafc with == as a guard --- + +(mk-test + "nafc-as-guard" + (run* + q + (fresh (x) (== x 5) (nafc (== x 99)) (== q x))) + (list 5)) + +(mk-test + "nafc-guard-blocking" + (run* + q + (fresh (x) (== x 5) (nafc (== x 5)) (== q x))) + (list)) + +(mk-tests-run!) diff --git a/plans/minikanren-on-sx.md b/plans/minikanren-on-sx.md index e3de93d6..80984e36 100644 --- a/plans/minikanren-on-sx.md +++ b/plans/minikanren-on-sx.md @@ -128,7 +128,9 @@ Key semantic mappings: - [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) -- [ ] `nafc` — negation as finite failure with constraint +- [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) @@ -157,6 +159,9 @@ _(none yet)_ _Newest first._ +- **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 "!")`).