From c6f58116bf829fe043bf01e7931e0076979dce7b Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 25 Apr 2026 08:39:32 +0000 Subject: [PATCH] prolog: copy_term/2 + functor/3 + arg/3, 14 tests; =.. deferred --- lib/prolog/conformance.sh | 1 + lib/prolog/runtime.sx | 104 ++++++++++++++++++++++ lib/prolog/scoreboard.json | 8 +- lib/prolog/scoreboard.md | 5 +- lib/prolog/tests/term_inspect.sx | 147 +++++++++++++++++++++++++++++++ plans/prolog-on-sx.md | 3 +- 6 files changed, 261 insertions(+), 7 deletions(-) create mode 100644 lib/prolog/tests/term_inspect.sx diff --git a/lib/prolog/conformance.sh b/lib/prolog/conformance.sh index b063f8a9..d42bc0dc 100755 --- a/lib/prolog/conformance.sh +++ b/lib/prolog/conformance.sh @@ -23,6 +23,7 @@ SUITES=( "operators:lib/prolog/tests/operators.sx:pl-operators-tests-run!" "dynamic:lib/prolog/tests/dynamic.sx:pl-dynamic-tests-run!" "findall:lib/prolog/tests/findall.sx:pl-findall-tests-run!" + "term_inspect:lib/prolog/tests/term_inspect.sx:pl-term-inspect-tests-run!" "append:lib/prolog/tests/programs/append.sx:pl-append-tests-run!" "reverse:lib/prolog/tests/programs/reverse.sx:pl-reverse-tests-run!" "member:lib/prolog/tests/programs/member.sx:pl-member-tests-run!" diff --git a/lib/prolog/runtime.sx b/lib/prolog/runtime.sx index 1965d621..6f565a02 100644 --- a/lib/prolog/runtime.sx +++ b/lib/prolog/runtime.sx @@ -470,6 +470,90 @@ ((rl (pl-mk-list-term sorted (pl-nil-term)))) (pl-solve-eq! third-rt rl trail k)))))))) +(define + pl-solve-eq2! + (fn + (a1 b1 a2 b2 trail k) + (let + ((mark (pl-trail-mark trail))) + (cond + ((and (pl-unify! a1 b1 trail) (pl-unify! a2 b2 trail)) + (let + ((r (k))) + (cond + (r true) + (true (begin (pl-trail-undo-to! trail mark) false))))) + (true (begin (pl-trail-undo-to! trail mark) false)))))) + +(define + pl-make-fresh-args + (fn + (n) + (cond + ((<= n 0) (list)) + (true (cons (pl-mk-rt-var "_") (pl-make-fresh-args (- n 1))))))) + +(define + pl-solve-functor-construct! + (fn + (term-rt name-rt arity-rt trail k) + (let + ((wn (pl-walk name-rt)) (wa (pl-walk arity-rt))) + (cond + ((and (pl-num? wa) (= (pl-num-val wa) 0)) + (cond + ((or (pl-atom? wn) (pl-num? wn)) + (pl-solve-eq! term-rt wn trail k)) + (true false))) + ((and (pl-num? wa) (> (pl-num-val wa) 0) (pl-atom? wn)) + (let + ((new-args (pl-make-fresh-args (pl-num-val wa)))) + (pl-solve-eq! + term-rt + (list "compound" (pl-atom-name wn) new-args) + trail + k))) + (true false))))) + +(define + pl-solve-functor! + (fn + (term-rt name-rt arity-rt trail k) + (let + ((wt (pl-walk term-rt))) + (cond + ((pl-var? wt) + (pl-solve-functor-construct! term-rt name-rt arity-rt trail k)) + ((pl-atom? wt) + (pl-solve-eq2! name-rt wt arity-rt (list "num" 0) trail k)) + ((pl-num? wt) + (pl-solve-eq2! name-rt wt arity-rt (list "num" 0) trail k)) + ((pl-compound? wt) + (pl-solve-eq2! + name-rt + (list "atom" (pl-fun wt)) + arity-rt + (list "num" (len (pl-args wt))) + trail + k)) + (true false))))) + +(define + pl-solve-arg! + (fn + (n-rt term-rt arg-rt trail k) + (let + ((wn (pl-walk n-rt)) (wt (pl-walk term-rt))) + (cond + ((and (pl-num? wn) (pl-compound? wt)) + (let + ((idx (pl-num-val wn)) (args (pl-args wt))) + (cond + ((and (>= idx 1) (<= idx (len args))) + (pl-solve-eq! arg-rt (nth args (- idx 1)) trail k)) + (true false)))) + (true false))))) + (define pl-retract-try-each (fn @@ -622,6 +706,26 @@ (nth (pl-args g) 2) trail k)) + ((and (pl-compound? g) (= (pl-fun g) "copy_term") (= (len (pl-args g)) 2)) + (pl-solve-eq! + (nth (pl-args g) 1) + (pl-deep-copy (first (pl-args g)) {}) + trail + k)) + ((and (pl-compound? g) (= (pl-fun g) "functor") (= (len (pl-args g)) 3)) + (pl-solve-functor! + (first (pl-args g)) + (nth (pl-args g) 1) + (nth (pl-args g) 2) + trail + k)) + ((and (pl-compound? g) (= (pl-fun g) "arg") (= (len (pl-args g)) 3)) + (pl-solve-arg! + (first (pl-args g)) + (nth (pl-args g) 1) + (nth (pl-args g) 2) + trail + k)) (true (pl-solve-user! db g trail cut-box k)))))) (define diff --git a/lib/prolog/scoreboard.json b/lib/prolog/scoreboard.json index b33461a6..6c2ea2a6 100644 --- a/lib/prolog/scoreboard.json +++ b/lib/prolog/scoreboard.json @@ -1,7 +1,7 @@ { - "total_passed": 224, + "total_passed": 238, "total_failed": 0, - "total": 224, - "suites": {"parse":{"passed":25,"total":25,"failed":0},"unify":{"passed":47,"total":47,"failed":0},"clausedb":{"passed":14,"total":14,"failed":0},"solve":{"passed":62,"total":62,"failed":0},"operators":{"passed":19,"total":19,"failed":0},"dynamic":{"passed":11,"total":11,"failed":0},"findall":{"passed":11,"total":11,"failed":0},"append":{"passed":6,"total":6,"failed":0},"reverse":{"passed":6,"total":6,"failed":0},"member":{"passed":7,"total":7,"failed":0},"nqueens":{"passed":6,"total":6,"failed":0},"family":{"passed":10,"total":10,"failed":0}}, - "generated": "2026-04-25T08:06:14+00:00" + "total": 238, + "suites": {"parse":{"passed":25,"total":25,"failed":0},"unify":{"passed":47,"total":47,"failed":0},"clausedb":{"passed":14,"total":14,"failed":0},"solve":{"passed":62,"total":62,"failed":0},"operators":{"passed":19,"total":19,"failed":0},"dynamic":{"passed":11,"total":11,"failed":0},"findall":{"passed":11,"total":11,"failed":0},"term_inspect":{"passed":14,"total":14,"failed":0},"append":{"passed":6,"total":6,"failed":0},"reverse":{"passed":6,"total":6,"failed":0},"member":{"passed":7,"total":7,"failed":0},"nqueens":{"passed":6,"total":6,"failed":0},"family":{"passed":10,"total":10,"failed":0}}, + "generated": "2026-04-25T08:39:07+00:00" } diff --git a/lib/prolog/scoreboard.md b/lib/prolog/scoreboard.md index 84f5b4b6..ef861cb1 100644 --- a/lib/prolog/scoreboard.md +++ b/lib/prolog/scoreboard.md @@ -1,7 +1,7 @@ # Prolog scoreboard -**224 / 224 passing** (0 failure(s)). -Generated 2026-04-25T08:06:14+00:00. +**238 / 238 passing** (0 failure(s)). +Generated 2026-04-25T08:39:07+00:00. | Suite | Passed | Total | Status | |-------|--------|-------|--------| @@ -12,6 +12,7 @@ Generated 2026-04-25T08:06:14+00:00. | operators | 19 | 19 | ok | | dynamic | 11 | 11 | ok | | findall | 11 | 11 | ok | +| term_inspect | 14 | 14 | ok | | append | 6 | 6 | ok | | reverse | 6 | 6 | ok | | member | 7 | 7 | ok | diff --git a/lib/prolog/tests/term_inspect.sx b/lib/prolog/tests/term_inspect.sx new file mode 100644 index 00000000..ca207db7 --- /dev/null +++ b/lib/prolog/tests/term_inspect.sx @@ -0,0 +1,147 @@ +;; lib/prolog/tests/term_inspect.sx — copy_term/2, functor/3, arg/3. + +(define pl-tt-test-count 0) +(define pl-tt-test-pass 0) +(define pl-tt-test-fail 0) +(define pl-tt-test-failures (list)) + +(define + pl-tt-test! + (fn + (name got expected) + (begin + (set! pl-tt-test-count (+ pl-tt-test-count 1)) + (if + (= got expected) + (set! pl-tt-test-pass (+ pl-tt-test-pass 1)) + (begin + (set! pl-tt-test-fail (+ pl-tt-test-fail 1)) + (append! + pl-tt-test-failures + (str name "\n expected: " expected "\n got: " got))))))) + +(define + pl-tt-goal + (fn + (src env) + (pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env))) + +(define pl-tt-db (pl-mk-db)) + +;; ── copy_term/2 ── + +(pl-tt-test! + "copy_term ground compound succeeds + copy = original" + (pl-solve-once! + pl-tt-db + (pl-tt-goal "copy_term(foo(a, b), X), X = foo(a, b)" {}) + (pl-mk-trail)) + true) + +(pl-tt-test! + "copy_term preserves var aliasing in source" + (pl-solve-once! + pl-tt-db + (pl-tt-goal "copy_term(p(Y, Y), p(A, B)), A = 5, B = 5" {}) + (pl-mk-trail)) + true) + +(pl-tt-test! + "copy_term distinct vars stay distinct" + (pl-solve-once! + pl-tt-db + (pl-tt-goal "copy_term(p(Y, Y), p(A, B)), A = 5, B = 6" {}) + (pl-mk-trail)) + false) + +(define pl-tt-env-1 {}) +(pl-solve-once! + pl-tt-db + (pl-tt-goal "copy_term(X, Y), Y = 5" pl-tt-env-1) + (pl-mk-trail)) +(pl-tt-test! + "copy_term: binding the copy doesn't bind the source" + (pl-var-bound? (dict-get pl-tt-env-1 "X")) + false) + +;; ── functor/3 ── + +(define pl-tt-env-2 {}) +(pl-solve-once! + pl-tt-db + (pl-tt-goal "functor(foo(a, b, c), F, N)" pl-tt-env-2) + (pl-mk-trail)) +(pl-tt-test! + "functor of compound: F = foo" + (pl-atom-name (pl-walk-deep (dict-get pl-tt-env-2 "F"))) + "foo") +(pl-tt-test! + "functor of compound: N = 3" + (pl-num-val (pl-walk-deep (dict-get pl-tt-env-2 "N"))) + 3) + +(define pl-tt-env-3 {}) +(pl-solve-once! + pl-tt-db + (pl-tt-goal "functor(hello, F, N)" pl-tt-env-3) + (pl-mk-trail)) +(pl-tt-test! + "functor of atom: F = hello" + (pl-atom-name (pl-walk-deep (dict-get pl-tt-env-3 "F"))) + "hello") +(pl-tt-test! + "functor of atom: N = 0" + (pl-num-val (pl-walk-deep (dict-get pl-tt-env-3 "N"))) + 0) + +(pl-tt-test! + "functor construct compound: T unifies with foo(a, b)" + (pl-solve-once! + pl-tt-db + (pl-tt-goal "functor(T, foo, 2), T = foo(a, b)" {}) + (pl-mk-trail)) + true) + +(pl-tt-test! + "functor construct atom: T = hello" + (pl-solve-once! + pl-tt-db + (pl-tt-goal "functor(T, hello, 0), T = hello" {}) + (pl-mk-trail)) + true) + +;; ── arg/3 ── + +(pl-tt-test! + "arg(1, foo(a, b, c), a)" + (pl-solve-once! + pl-tt-db + (pl-tt-goal "arg(1, foo(a, b, c), a)" {}) + (pl-mk-trail)) + true) + +(pl-tt-test! + "arg(2, foo(a, b, c), X) → X = b" + (pl-solve-once! + pl-tt-db + (pl-tt-goal "arg(2, foo(a, b, c), X), X = b" {}) + (pl-mk-trail)) + true) + +(pl-tt-test! + "arg out-of-range high fails" + (pl-solve-once! + pl-tt-db + (pl-tt-goal "arg(4, foo(a, b, c), X)" {}) + (pl-mk-trail)) + false) + +(pl-tt-test! + "arg(0, ...) fails (1-indexed)" + (pl-solve-once! + pl-tt-db + (pl-tt-goal "arg(0, foo(a), X)" {}) + (pl-mk-trail)) + false) + +(define pl-term-inspect-tests-run! (fn () {:failed pl-tt-test-fail :passed pl-tt-test-pass :total pl-tt-test-count :failures pl-tt-test-failures})) diff --git a/plans/prolog-on-sx.md b/plans/prolog-on-sx.md index 88bc4aef..909ada79 100644 --- a/plans/prolog-on-sx.md +++ b/plans/prolog-on-sx.md @@ -68,7 +68,7 @@ Representation choices (finalise in phase 1, document here): - [x] Operator table parsing (prefix/infix/postfix, precedence, assoc) — `pl-op-table` (15 entries: `, ; -> = \= is < > =< >= + - * / mod`); precedence-climbing parser via `pp-parse-primary` + `pp-parse-term-prec` + `pp-parse-op-rhs`. Parens override precedence. Args inside compounds parsed at 999 so `,` stays as separator. xfx/xfy/yfx supported; prefix/postfix deferred (so `-5` still tokenises as bare atom + num as before). Comparison built-ins `/2 ==/2` added. New `tests/operators.sx` 19 tests cover assoc/precedence/parens + solver via infix. - [x] `assert/1`, `asserta/1`, `assertz/1`, `retract/1` — `assert` aliases `assertz`. Helpers `pl-rt-to-ast` (deep-walk + replace runtime vars with `_G` parse markers) + `pl-build-clause` (detect `:-` head). `assertz` uses `pl-db-add!`; `asserta` uses new `pl-db-prepend!`. `retract` walks goal, looks up by functor/arity, tries each clause via unification, removes first match by index (`pl-list-without`). 11 tests in `tests/dynamic.sx`. Rule-asserts deferred — `:-` not in op table yet, so only fact-shaped clauses for now. - [x] `findall/3`, `bagof/3`, `setof/3` — shared `pl-collect-solutions` runs the goal in a fresh cut-box, deep-copies the template (via `pl-deep-copy` with var-map for shared-var preservation) on each success, returns false to backtrack, then restores trail. `findall` always succeeds with a (possibly empty) list. `bagof` fails on empty. `setof` builds a string-keyed dict via `pl-format-term` for sort+dedupe (via `keys` + `sort`), fails on empty. Existential `^` deferred (operator). 11 tests in `tests/findall.sx`. -- [ ] `copy_term/2`, `functor/3`, `arg/3`, `=../2` +- [x] `copy_term/2`, `functor/3`, `arg/3`, `=../2` — `copy_term/2` reuses `pl-deep-copy` with a fresh var-map (preserves source aliasing). `functor/3` handles 4 modes: compound→{name, arity}, atom→{atom, 0}, num→{num, 0}, var with ground name+arity→constructed term (`pl-make-fresh-args` for compound case). `arg/3` extracts 1-indexed arg from compound. **`=../2` deferred** — the tokenizer treats `.` as the clause terminator unconditionally, so `=..` lexes as `=` + `.` + `.`; needs special-case lex (or surface syntax via a different name). 14 tests in `tests/term_inspect.sx`. - [ ] String/atom predicates ### Phase 5 — Hyperscript integration @@ -88,6 +88,7 @@ Representation choices (finalise in phase 1, document here): _Newest first. Agent appends on every commit._ +- 2026-04-25 — `copy_term/2` + `functor/3` + `arg/3` (term inspection). `copy_term` is a one-line dispatch to existing `pl-deep-copy`. `functor/3` is bidirectional — decomposes a bound compound/atom/num into name+arity OR constructs from ground name+arity (atom+positive-arity → compound with N anonymous fresh args via `pl-make-fresh-args`; arity 0 → atom/num). `arg/3` extracts 1-indexed arg with bounds-fail. New helper `pl-solve-eq2!` for paired-unification with shared trail-undo. 14 tests in `tests/term_inspect.sx`. Total **238** (+14). `=..` deferred — `.` always tokenizes as clause terminator; needs special lexer case. - 2026-04-25 — `findall/3` + `bagof/3` + `setof/3`. Shared collector `pl-collect-solutions` runs the goal in a fresh cut-box, deep-copies the template per success (`pl-deep-copy` walks term, allocates fresh runtime vars via shared var-map so co-occurrences keep aliasing), returns false to keep backtracking, then `pl-trail-undo-to!` to clean up. `findall` always builds a list. `bagof` fails on empty. `setof` uses a `pl-format-term`-keyed dict + SX `sort` for dedupe + ordering. New `tests/findall.sx` 11 tests. Total **224** (+11). Existential `^` deferred — needs operator. - 2026-04-25 — Dynamic clauses: `assert/1`, `assertz/1`, `asserta/1`, `retract/1`. New helpers `pl-rt-to-ast` (deep-walk runtime term → parse-AST, mapping unbound runtime vars to `_G` markers so `pl-instantiate-fresh` produces fresh vars per call) + `pl-build-clause` + `pl-db-prepend!` + `pl-list-without`. `retract` keeps runtime vars (so the caller's vars get bound), walks head for the functor/arity key, tries each stored clause via `pl-unify!`, removes the first match by index. 11 tests in `tests/dynamic.sx`; conformance script gained dynamic row. Total **213** (+11). Rule-form asserts (`(H :- B)`) deferred until `:-` is in the op table. - 2026-04-25 — Phase 4 starts: operator-table parsing. Parser rewrite uses precedence climbing (xfx/xfy/yfx); 15-op table covers control (`, ; ->`), comparison (`= \\= is < > =< >=`), arithmetic (`+ - * / mod`). Parens override. Backwards-compatible: prefix-syntax compounds (`=(X, Y)`, `+(2, 3)`) still parse as before; existing 183 tests untouched. Added comparison built-ins `/2 ==/2` to runtime (eval both sides, compare). New `tests/operators.sx` 19 tests; conformance script gained an operators row. Total **202** (+19). Prefix/postfix deferred — `-5` keeps old bare-atom semantics.