diff --git a/lib/prolog/runtime.sx b/lib/prolog/runtime.sx index 8175426d..596f051f 100644 --- a/lib/prolog/runtime.sx +++ b/lib/prolog/runtime.sx @@ -299,6 +299,8 @@ ((pl-cut? g) (begin (dict-set! cut-box :cut true) (k))) ((and (pl-atom? g) (= (pl-atom-name g) "true")) (k)) ((and (pl-atom? g) (= (pl-atom-name g) "fail")) false) + ((and (pl-atom? g) (= (pl-atom-name g) "nl")) + (begin (pl-output-write! "\n") (k))) ((and (pl-compound? g) (= (pl-fun g) "=") (= (len (pl-args g)) 2)) (pl-solve-eq! (first (pl-args g)) (nth (pl-args g) 1) trail k)) ((and (pl-compound? g) (= (pl-fun g) "\\=") (= (len (pl-args g)) 2)) @@ -335,6 +337,10 @@ (let ((call-cb {:cut false})) (pl-solve! db (first (pl-args g)) trail call-cb k))) + ((and (pl-compound? g) (= (pl-fun g) "write") (= (len (pl-args g)) 1)) + (begin + (pl-output-write! (pl-format-term (first (pl-args g)))) + (k))) (true (pl-solve-user! db g trail cut-box k)))))) (define @@ -387,6 +393,42 @@ (pl-trail-undo-to! trail mark) (pl-solve! db else-goal trail cut-box k))))))))) +(define pl-output-buffer "") + +(define pl-output-clear! (fn () (set! pl-output-buffer ""))) + +(define + pl-output-write! + (fn (s) (begin (set! pl-output-buffer (str pl-output-buffer s)) nil))) + +(define + pl-format-args + (fn + (args) + (cond + ((empty? args) "") + ((= (len args) 1) (pl-format-term (first args))) + (true + (str + (pl-format-term (first args)) + ", " + (pl-format-args (rest args))))))) + +(define + pl-format-term + (fn + (t) + (let + ((w (pl-walk-deep t))) + (cond + ((pl-var? w) (str "_" (pl-var-id w))) + ((pl-atom? w) (pl-atom-name w)) + ((pl-num? w) (str (pl-num-val w))) + ((pl-str? w) (pl-str-val w)) + ((pl-compound? w) + (str (pl-fun w) "(" (pl-format-args (pl-args w)) ")")) + (true (str w)))))) + (define pl-solve-not-eq! (fn diff --git a/lib/prolog/tests/solve.sx b/lib/prolog/tests/solve.sx index a64ce5f2..59257757 100644 --- a/lib/prolog/tests/solve.sx +++ b/lib/prolog/tests/solve.sx @@ -421,4 +421,78 @@ (pl-mk-trail)) false) +(pl-s-test! + "write(hello)" + (begin + (pl-output-clear!) + (pl-solve-once! + pl-s-empty-db + (pl-s-goal "write(hello)" {}) + (pl-mk-trail)) + pl-output-buffer) + "hello") + +(pl-s-test! + "nl outputs newline" + (begin + (pl-output-clear!) + (pl-solve-once! pl-s-empty-db (pl-s-goal "nl" {}) (pl-mk-trail)) + pl-output-buffer) + "\n") + +(pl-s-test! + "write(42) outputs digits" + (begin + (pl-output-clear!) + (pl-solve-once! + pl-s-empty-db + (pl-s-goal "write(42)" {}) + (pl-mk-trail)) + pl-output-buffer) + "42") + +(pl-s-test! + "write(foo(a, b)) formats compound" + (begin + (pl-output-clear!) + (pl-solve-once! + pl-s-empty-db + (pl-s-goal "write(foo(a, b))" {}) + (pl-mk-trail)) + pl-output-buffer) + "foo(a, b)") + +(pl-s-test! + "write conjunction" + (begin + (pl-output-clear!) + (pl-solve-once! + pl-s-empty-db + (pl-s-goal "write(a), write(b)" {}) + (pl-mk-trail)) + pl-output-buffer) + "ab") + +(pl-s-test! + "write of bound var walks binding" + (begin + (pl-output-clear!) + (pl-solve-once! + pl-s-empty-db + (pl-s-goal "=(X, hello), write(X)" {}) + (pl-mk-trail)) + pl-output-buffer) + "hello") + +(pl-s-test! + "write then nl" + (begin + (pl-output-clear!) + (pl-solve-once! + pl-s-empty-db + (pl-s-goal "write(hi), nl" {}) + (pl-mk-trail)) + pl-output-buffer) + "hi\n") + (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 7239bed6..336d7e39 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`, `->/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. +- [x] Built-ins: `=/2`, `\\=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `;/2`, `->/2` inside `;`, `call/1`, `write/1`, `nl/0` — all 11 done. `write/1` and `nl/0` use a global `pl-output-buffer` string + `pl-output-clear!` for testability; `pl-format-term` walks deep then renders atoms/nums/strs/compounds/vars (var → `_`). Note: cut-transparency via `;` not testable yet without operator support — `;(,(a,!), b)` parser-rejects because `,` is body-operator-only; revisit in phase 4. - [ ] 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 — `write/1` + `nl/0` landed using global string buffer (`pl-output-buffer` + `pl-output-clear!` + `pl-output-write!`). `pl-format-term` walks deep + dispatches on atom/num/str/compound/var; `pl-format-args` recursively comma-joins. 7 new tests cover atom/num/compound formatting, conjunction order, var-walk, and `nl`. Built-ins box (`=/2`, `\\=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `;/2`, `->/2`, `call/1`, `write/1`, `nl/0`) now ticked. Total 137 (+7). - 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).