diff --git a/lib/prolog/runtime.sx b/lib/prolog/runtime.sx index 623d91d9..8175426d 100644 --- a/lib/prolog/runtime.sx +++ b/lib/prolog/runtime.sx @@ -322,12 +322,71 @@ trail cut-box k)) + ((and (pl-compound? g) (= (pl-fun g) "->") (= (len (pl-args g)) 2)) + (pl-solve-if-then-else! + db + (first (pl-args g)) + (nth (pl-args g) 1) + (list "atom" "fail") + trail + cut-box + k)) ((and (pl-compound? g) (= (pl-fun g) "call") (= (len (pl-args g)) 1)) (let ((call-cb {:cut false})) (pl-solve! db (first (pl-args g)) trail call-cb k))) (true (pl-solve-user! db g trail cut-box k)))))) +(define + pl-solve-or! + (fn + (db a b trail cut-box k) + (cond + ((and (pl-compound? a) (= (pl-fun a) "->") (= (len (pl-args a)) 2)) + (pl-solve-if-then-else! + db + (first (pl-args a)) + (nth (pl-args a) 1) + b + trail + cut-box + k)) + (true + (let + ((mark (pl-trail-mark trail))) + (let + ((r (pl-solve! db a trail cut-box k))) + (cond + (r true) + ((dict-get cut-box :cut) false) + (true + (begin + (pl-trail-undo-to! trail mark) + (pl-solve! db b trail cut-box k)))))))))) + +(define + pl-solve-if-then-else! + (fn + (db cond-goal then-goal else-goal trail cut-box k) + (let + ((mark (pl-trail-mark trail))) + (let + ((local-cb {:cut false})) + (let + ((found {:val false})) + (pl-solve! + db + cond-goal + trail + local-cb + (fn () (begin (dict-set! found :val true) true))) + (cond + ((dict-get found :val) (pl-solve! db then-goal trail cut-box k)) + (true + (begin + (pl-trail-undo-to! trail mark) + (pl-solve! db else-goal trail cut-box k))))))))) + (define pl-solve-not-eq! (fn @@ -340,22 +399,6 @@ (pl-trail-undo-to! trail mark) (cond (unified false) (true (k)))))))) -(define - pl-solve-or! - (fn - (db a b trail cut-box k) - (let - ((mark (pl-trail-mark trail))) - (let - ((r (pl-solve! db a trail cut-box k))) - (cond - (r true) - ((dict-get cut-box :cut) false) - (true - (begin - (pl-trail-undo-to! trail mark) - (pl-solve! db b trail cut-box k)))))))) - (define pl-solve-eq! (fn diff --git a/lib/prolog/tests/solve.sx b/lib/prolog/tests/solve.sx index aacaefbd..a64ce5f2 100644 --- a/lib/prolog/tests/solve.sx +++ b/lib/prolog/tests/solve.sx @@ -337,4 +337,88 @@ (pl-mk-trail)) true) +(define pl-s-db-ite (pl-mk-db)) + +(pl-db-load! pl-s-db-ite (pl-parse "p(1). p(2). q(yes). q(no).")) + +(pl-s-test! + "if-then-else: cond true → then runs" + (pl-solve-once! + pl-s-db-ite + (pl-s-goal ";(->(true, =(X, ok)), =(X, fallback))" {}) + (pl-mk-trail)) + true) + +(define pl-s-env-ite1 {}) + +(pl-solve-once! + pl-s-db-ite + (pl-s-goal ";(->(true, =(X, ok)), =(X, fallback))" pl-s-env-ite1) + (pl-mk-trail)) + +(pl-s-test! + "if-then-else: cond true binds via then" + (pl-atom-name (pl-walk-deep (dict-get pl-s-env-ite1 "X"))) + "ok") + +(pl-s-test! + "if-then-else: cond false → else" + (pl-solve-once! + pl-s-db-ite + (pl-s-goal ";(->(fail, =(X, ok)), =(X, fallback))" {}) + (pl-mk-trail)) + true) + +(define pl-s-env-ite2 {}) + +(pl-solve-once! + pl-s-db-ite + (pl-s-goal ";(->(fail, =(X, ok)), =(X, fallback))" pl-s-env-ite2) + (pl-mk-trail)) + +(pl-s-test! + "if-then-else: cond false binds via else" + (pl-atom-name (pl-walk-deep (dict-get pl-s-env-ite2 "X"))) + "fallback") + +(pl-s-test! + "if-then-else: cond commits to first solution (count = 1)" + (pl-solve-count! + pl-s-db-ite + (pl-s-goal ";(->(p(X), =(Y, found)), =(Y, none))" {}) + (pl-mk-trail)) + 1) + +(pl-s-test! + "if-then-else: then can backtrack" + (pl-solve-count! + pl-s-db-ite + (pl-s-goal ";(->(true, p(X)), =(X, none))" {}) + (pl-mk-trail)) + 2) + +(pl-s-test! + "if-then-else: else can backtrack" + (pl-solve-count! + pl-s-db-ite + (pl-s-goal ";(->(fail, =(X, ignored)), p(X))" {}) + (pl-mk-trail)) + 2) + +(pl-s-test! + "standalone -> with true cond succeeds" + (pl-solve-once! + pl-s-db-ite + (pl-s-goal "->(true, =(X, hi))" {}) + (pl-mk-trail)) + true) + +(pl-s-test! + "standalone -> with false cond fails" + (pl-solve-once! + pl-s-db-ite + (pl-s-goal "->(fail, =(X, hi))" {}) + (pl-mk-trail)) + false) + (define pl-solve-tests-run! (fn () {:failed pl-s-test-fail :passed pl-s-test-pass :total pl-s-test-count :failures pl-s-test-failures})) diff --git a/plans/prolog-on-sx.md b/plans/prolog-on-sx.md index f08507de..7239bed6 100644 --- a/plans/prolog-on-sx.md +++ b/plans/prolog-on-sx.md @@ -53,7 +53,7 @@ Representation choices (finalise in phase 1, document here): - [x] Clause DB: `"functor/arity" → list-of-clauses`, loader inserts — `pl-mk-db` / `pl-db-add!` / `pl-db-load!` / `pl-db-lookup` / `pl-db-lookup-goal`, 14 tests in `tests/clausedb.sx` - [x] Solver: DFS with choice points backed by delimited continuations (`lib/callcc.sx`). On goal entry, capture; per matching clause, unify head + recurse body; on failure, undo trail, try next — first cut: trail-based undo + CPS k (no shift/reset yet, per briefing gotcha). Built-ins so far: `true/0`, `fail/0`, `=/2`, `,/2`. Refactor to delimited conts later. - [x] Cut (`!`): cut barrier at current choice-point frame; collapse all up to barrier — two-cut-box scheme: each `pl-solve-user!` creates a fresh inner-cut-box (set by `!` in this predicate's body) AND snapshots the outer-cut-box state on entry. After body fails, abandon clause alternatives if (a) inner was set or (b) outer transitioned false→true during this call. Lets post-cut goals backtrack normally while blocking pre-cut alternatives. 6 cut tests cover bare cut, clause-commit, choice-commit, cut+fail, post-cut backtracking, nested-cut isolation. -- [ ] Built-ins: `=/2`, `\\=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `;/2`, `->/2` inside `;`, `call/1`, `write/1`, `nl/0` — done so far: `=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `\\=/2`, `;/2`, `call/1`. Pending: `->/2` inside `;`, `write/1`, `nl/0`. Note: cut-transparency via `;` not testable yet without operator support — `;(,(a,!), b)` parser-rejects because `,` is body-operator-only. +- [ ] Built-ins: `=/2`, `\\=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `;/2`, `->/2` inside `;`, `call/1`, `write/1`, `nl/0` — done so far: `=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `\\=/2`, `;/2`, `call/1`, `->/2` (both inside `;` and standalone). Pending: `write/1`, `nl/0`. Note: cut-transparency via `;` not testable yet without operator support — `;(,(a,!), b)` parser-rejects because `,` is body-operator-only. - [ ] Arithmetic `is/2` with `+ - * / mod abs` - [ ] Classic programs in `lib/prolog/tests/programs/`: - [ ] `append.pl` — list append (with backtracking) @@ -88,6 +88,7 @@ Representation choices (finalise in phase 1, document here): _Newest first. Agent appends on every commit._ +- 2026-04-25 — `->/2` if-then-else landed (both `;(->(C,T), E)` and standalone `->(C, T)` ≡ `(C -> T ; fail)`). `pl-solve-or!` now special-cases `->` in left arg → `pl-solve-if-then-else!`. Cond runs in a fresh local cut-box (ISO opacity for cut inside cond). Then-branch can backtrack, else-branch can backtrack, but cond commits to first solution. 9 new tests covering both forms, both branches, binding visibility, cond-commit, then-backtrack, else-backtrack. Total 130 (+9). - 2026-04-25 — Built-ins `\=/2`, `;/2`, `call/1` landed. `pl-solve-not-eq!` (try unify, always undo, succeed iff unify failed). `pl-solve-or!` (try left, on failure check cut and only try right if not cut). `call/1` opens a fresh inner cut-box (ISO opacity: cut inside `call(G)` commits G, not caller). 11 new tests in `tests/solve.sx` cover atoms+vars for `\=`, both branches + count for `;`, and `call/1` against atoms / compounds / bound goal vars. Total 121 (+11). Box not yet ticked — `->/2`, `write/1`, `nl/0` still pending. - 2026-04-25 — Cut (`!/0`) landed. `pl-cut?` predicate; solver functions all take a `cut-box`; `pl-solve-user!` creates a fresh inner-cut-box and snapshots `outer-was-cut`; `pl-try-clauses!` abandons alternatives when inner.cut OR (outer.cut transitioned false→true during this call). 6 new cut tests in `tests/solve.sx` covering bare cut, clause-commit, choice-commit, cut+fail blocks alt clauses, post-cut goal backtracks freely, inner cut isolation. Total 110 (+6). - 2026-04-25 — Phase 3 DFS solver landed (CPS, trail-based backtracking; delimited conts deferred). `pl-solve!` + `pl-solve-eq!` + `pl-solve-user!` + `pl-try-clauses!` + `pl-solve-once!` + `pl-solve-count!` in runtime.sx. Built-ins: `true/0`, `fail/0`, `=/2`, `,/2`. New `tests/solve.sx` 18/18 green covers atomic goals, =, conjunction, fact lookup, multi-solution count, recursive ancestor rule, trail-undo verification. Bug fix: `pl-instantiate` had no `("clause" h b)` case → vars in rule head/body were never instantiated, so rule resolution silently failed against runtime-var goals. Added clause case to recurse with shared var-env. Total 104 (+18).