diff --git a/lib/prolog/conformance.sh b/lib/prolog/conformance.sh index e30af481..0a963778 100755 --- a/lib/prolog/conformance.sh +++ b/lib/prolog/conformance.sh @@ -39,6 +39,7 @@ SUITES=( "char_predicates:lib/prolog/tests/char_predicates.sx:pl-char-predicates-tests-run!" "io_predicates:lib/prolog/tests/io_predicates.sx:pl-io-predicates-tests-run!" "assert_rules:lib/prolog/tests/assert_rules.sx:pl-assert-rules-tests-run!" + "string_agg:lib/prolog/tests/string_agg.sx:pl-string-agg-tests-run!" ) SCRIPT='(epoch 1) diff --git a/lib/prolog/runtime.sx b/lib/prolog/runtime.sx index 4e6f77a7..74581361 100644 --- a/lib/prolog/runtime.sx +++ b/lib/prolog/runtime.sx @@ -1517,6 +1517,139 @@ false)))) +(define + pl-substring + (fn (s start sublen) (substring s start (+ start sublen)))) + +(define + pl-sub-atom-try-one! + (fn + (s start sublen total-len before-arg len-arg after-arg sub-arg trail k) + (let + ((mark (pl-trail-mark trail)) + (after-val (- total-len (+ start sublen))) + (sub (pl-substring s start sublen))) + (if + (and + (pl-unify! before-arg (list "num" start) trail) + (pl-unify! len-arg (list "num" sublen) trail) + (pl-unify! after-arg (list "num" after-val) trail) + (pl-unify! sub-arg (list "atom" sub) trail)) + (let + ((kresult (k))) + (if kresult kresult (begin (pl-trail-undo-to! trail mark) false))) + (begin (pl-trail-undo-to! trail mark) false))))) + +(define + pl-sub-atom-loop! + (fn + (s total-len start sublen before-arg len-arg after-arg sub-arg trail k) + (cond + ((> start total-len) false) + ((> sublen (- total-len start)) + (pl-sub-atom-loop! + s + total-len + (+ start 1) + 0 + before-arg + len-arg + after-arg + sub-arg + trail + k)) + (true + (let + ((one-result (pl-sub-atom-try-one! s start sublen total-len before-arg len-arg after-arg sub-arg trail k))) + (if + one-result + one-result + (pl-sub-atom-loop! + s + total-len + start + (+ sublen 1) + before-arg + len-arg + after-arg + sub-arg + trail + k))))))) + +(define + pl-solve-aggregate-all! + (fn + (db tmpl goal result trail k) + (let + ((tmpl-walked (pl-walk-deep tmpl))) + (cond + ((and (pl-atom? tmpl-walked) (= (pl-atom-name tmpl-walked) "count")) + (let + ((solutions (pl-collect-solutions db (list "atom" "true") goal trail))) + (if + (pl-unify! result (list "num" (len solutions)) trail) + (k) + false))) + ((and (pl-compound? tmpl-walked) (= (pl-fun tmpl-walked) "bag") (= (len (pl-args tmpl-walked)) 1)) + (let + ((template (nth (pl-args tmpl-walked) 0))) + (let + ((solutions (pl-collect-solutions db template goal trail))) + (let + ((prolog-list (pl-mk-list-term solutions (pl-nil-term)))) + (if (pl-unify! result prolog-list trail) (k) false))))) + ((and (pl-compound? tmpl-walked) (= (pl-fun tmpl-walked) "sum") (= (len (pl-args tmpl-walked)) 1)) + (let + ((template (nth (pl-args tmpl-walked) 0))) + (let + ((solutions (pl-collect-solutions db template goal trail))) + (let + ((total (reduce (fn (acc sol) (+ acc (pl-eval-arith sol))) 0 solutions))) + (if (pl-unify! result (list "num" total) trail) (k) false))))) + ((and (pl-compound? tmpl-walked) (= (pl-fun tmpl-walked) "max") (= (len (pl-args tmpl-walked)) 1)) + (let + ((template (nth (pl-args tmpl-walked) 0))) + (let + ((solutions (pl-collect-solutions db template goal trail))) + (if + (empty? solutions) + false + (let + ((vals (map pl-eval-arith solutions))) + (let + ((mx (reduce (fn (a b) (if (> a b) a b)) (first vals) (rest vals)))) + (if (pl-unify! result (list "num" mx) trail) (k) false))))))) + ((and (pl-compound? tmpl-walked) (= (pl-fun tmpl-walked) "min") (= (len (pl-args tmpl-walked)) 1)) + (let + ((template (nth (pl-args tmpl-walked) 0))) + (let + ((solutions (pl-collect-solutions db template goal trail))) + (if + (empty? solutions) + false + (let + ((vals (map pl-eval-arith solutions))) + (let + ((mn (reduce (fn (a b) (if (< a b) a b)) (first vals) (rest vals)))) + (if (pl-unify! result (list "num" mn) trail) (k) false))))))) + ((and (pl-compound? tmpl-walked) (= (pl-fun tmpl-walked) "set") (= (len (pl-args tmpl-walked)) 1)) + (let + ((template (nth (pl-args tmpl-walked) 0))) + (let + ((solutions (pl-collect-solutions db template goal trail))) + (let + ((deduped (pl-list-to-set-sx solutions (list)))) + (let + ((keyed (map (fn (t) (list (pl-format-term t) t)) deduped))) + (let + ((sorted (sort keyed))) + (let + ((sorted-terms (map (fn (pair) (nth pair 1)) sorted))) + (let + ((prolog-list (pl-mk-list-term sorted-terms (pl-nil-term)))) + (if (pl-unify! result prolog-list trail) (k) false))))))))) + (true false))))) + (define pl-solve! (fn @@ -2240,7 +2373,40 @@ ((and (pl-compound? g) (= (pl-fun g) "format") (= (len (pl-args g)) 1)) (pl-solve-format-1! (nth (pl-args g) 0) k)) ((and (pl-compound? g) (= (pl-fun g) "format") (= (len (pl-args g)) 2)) - (pl-solve-format-2! db (nth (pl-args g) 0) (nth (pl-args g) 1) trail k)) + (pl-solve-format-2! + db + (nth (pl-args g) 0) + (nth (pl-args g) 1) + trail + k)) + ((and (pl-compound? g) (= (pl-fun g) "sub_atom") (= (len (pl-args g)) 5)) + (let + ((atom-term (pl-walk-deep (nth (pl-args g) 0)))) + (if + (pl-atom? atom-term) + (let + ((s (pl-atom-name atom-term)) + (total-len (len (pl-atom-name atom-term)))) + (pl-sub-atom-loop! + s + total-len + 0 + 0 + (pl-walk (nth (pl-args g) 1)) + (pl-walk (nth (pl-args g) 2)) + (pl-walk (nth (pl-args g) 3)) + (pl-walk (nth (pl-args g) 4)) + trail + k)) + false))) + ((and (pl-compound? g) (= (pl-fun g) "aggregate_all") (= (len (pl-args g)) 3)) + (pl-solve-aggregate-all! + db + (pl-walk (nth (pl-args g) 0)) + (pl-walk (nth (pl-args g) 1)) + (pl-walk (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 04d96af5..d8032461 100644 --- a/lib/prolog/scoreboard.json +++ b/lib/prolog/scoreboard.json @@ -1,7 +1,7 @@ { - "total_passed": 471, + "total_passed": 496, "total_failed": 0, - "total": 471, - "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},"atoms":{"passed":34,"total":34,"failed":0},"query_api":{"passed":16,"total":16,"failed":0},"iso_predicates":{"passed":29,"total":29,"failed":0},"meta_predicates":{"passed":25,"total":25,"failed":0},"list_predicates":{"passed":33,"total":33,"failed":0},"meta_call":{"passed":15,"total":15,"failed":0},"set_predicates":{"passed":15,"total":15,"failed":0},"char_predicates":{"passed":27,"total":27,"failed":0},"io_predicates":{"passed":24,"total":24,"failed":0},"assert_rules":{"passed":15,"total":15,"failed":0}}, - "generated": "2026-04-25T13:21:37+00:00" + "total": 496, + "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},"atoms":{"passed":34,"total":34,"failed":0},"query_api":{"passed":16,"total":16,"failed":0},"iso_predicates":{"passed":29,"total":29,"failed":0},"meta_predicates":{"passed":25,"total":25,"failed":0},"list_predicates":{"passed":33,"total":33,"failed":0},"meta_call":{"passed":15,"total":15,"failed":0},"set_predicates":{"passed":15,"total":15,"failed":0},"char_predicates":{"passed":27,"total":27,"failed":0},"io_predicates":{"passed":24,"total":24,"failed":0},"assert_rules":{"passed":15,"total":15,"failed":0},"string_agg":{"passed":25,"total":25,"failed":0}}, + "generated": "2026-04-25T13:49:43+00:00" } diff --git a/lib/prolog/scoreboard.md b/lib/prolog/scoreboard.md index ec0c2b07..4f2ad17d 100644 --- a/lib/prolog/scoreboard.md +++ b/lib/prolog/scoreboard.md @@ -1,7 +1,7 @@ # Prolog scoreboard -**471 / 471 passing** (0 failure(s)). -Generated 2026-04-25T13:21:37+00:00. +**496 / 496 passing** (0 failure(s)). +Generated 2026-04-25T13:49:43+00:00. | Suite | Passed | Total | Status | |-------|--------|-------|--------| @@ -28,6 +28,7 @@ Generated 2026-04-25T13:21:37+00:00. | char_predicates | 27 | 27 | ok | | io_predicates | 24 | 24 | ok | | assert_rules | 15 | 15 | ok | +| string_agg | 25 | 25 | ok | Run `bash lib/prolog/conformance.sh` to refresh. Override the binary with `SX_SERVER=path/to/sx_server.exe bash …`. diff --git a/lib/prolog/tests/string_agg.sx b/lib/prolog/tests/string_agg.sx new file mode 100644 index 00000000..3ec3b2f6 --- /dev/null +++ b/lib/prolog/tests/string_agg.sx @@ -0,0 +1,273 @@ +;; lib/prolog/tests/string_agg.sx -- sub_atom/5 + aggregate_all/3 + +(define pl-sa-test-count 0) +(define pl-sa-test-pass 0) +(define pl-sa-test-fail 0) +(define pl-sa-test-failures (list)) + +(define + pl-sa-test! + (fn + (name got expected) + (begin + (set! pl-sa-test-count (+ pl-sa-test-count 1)) + (if + (= got expected) + (set! pl-sa-test-pass (+ pl-sa-test-pass 1)) + (begin + (set! pl-sa-test-fail (+ pl-sa-test-fail 1)) + (append! + pl-sa-test-failures + (str name "\n expected: " expected "\n got: " got))))))) + +(define + pl-sa-goal + (fn + (src env) + (pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env))) + +(define pl-sa-db (pl-mk-db)) + +(define + pl-sa-num-val + (fn (env key) (pl-num-val (pl-walk-deep (dict-get env key))))) + +(define + pl-sa-list-to-atoms + (fn + (t) + (let + ((w (pl-walk-deep t))) + (cond + ((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list)) + ((and (pl-compound? w) (= (pl-fun w) ".") (= (len (pl-args w)) 2)) + (cons + (pl-atom-name (first (pl-args w))) + (pl-sa-list-to-atoms (nth (pl-args w) 1)))) + (true (list)))))) + +(define pl-sa-prog-src "member(X, [X|_]). member(X, [_|T]) :- member(X, T).") +(pl-db-load! pl-sa-db (pl-parse pl-sa-prog-src)) + +;; -- sub_atom/5 -- + +(pl-sa-test! + "sub_atom ground: sub_atom(abcde,0,3,2,abc)" + (pl-solve-once! + pl-sa-db + (pl-sa-goal "sub_atom(abcde, 0, 3, 2, abc)" {}) + (pl-mk-trail)) + true) + +(pl-sa-test! + "sub_atom ground: sub_atom(abcde,2,2,1,cd)" + (pl-solve-once! + pl-sa-db + (pl-sa-goal "sub_atom(abcde, 2, 2, 1, cd)" {}) + (pl-mk-trail)) + true) + +(pl-sa-test! + "sub_atom ground mismatch fails" + (pl-solve-once! + pl-sa-db + (pl-sa-goal "sub_atom(abcde, 0, 2, 3, cd)" {}) + (pl-mk-trail)) + false) + +(pl-sa-test! + "sub_atom empty sub at start" + (pl-solve-once! + pl-sa-db + (pl-sa-goal "sub_atom(abcde, 0, 0, 5, '')" {}) + (pl-mk-trail)) + true) + +(pl-sa-test! + "sub_atom whole string" + (pl-solve-once! + pl-sa-db + (pl-sa-goal "sub_atom(hello, 0, 5, 0, hello)" {}) + (pl-mk-trail)) + true) + +(define pl-sa-env-b1 {}) +(pl-solve-once! + pl-sa-db + (pl-sa-goal "sub_atom(abcde, B, 2, A, cd)" pl-sa-env-b1) + (pl-mk-trail)) +(pl-sa-test! + "sub_atom bound SubAtom gives B=2" + (pl-sa-num-val pl-sa-env-b1 "B") + 2) +(pl-sa-test! + "sub_atom bound SubAtom gives A=1" + (pl-sa-num-val pl-sa-env-b1 "A") + 1) + +(define pl-sa-env-b2 {}) +(pl-solve-once! + pl-sa-db + (pl-sa-goal "sub_atom(hello, B, L, A, ello)" pl-sa-env-b2) + (pl-mk-trail)) +(pl-sa-test! "sub_atom ello: B=1" (pl-sa-num-val pl-sa-env-b2 "B") 1) +(pl-sa-test! "sub_atom ello: L=4" (pl-sa-num-val pl-sa-env-b2 "L") 4) +(pl-sa-test! "sub_atom ello: A=0" (pl-sa-num-val pl-sa-env-b2 "A") 0) + +(pl-sa-test! + "sub_atom ab: 6 total solutions" + (let + ((env {})) + (pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(count, sub_atom(ab, _, _, _, _), N)" env) + (pl-mk-trail)) + (pl-sa-num-val env "N")) + 6) + +(pl-sa-test! + "sub_atom a: 3 total solutions" + (let + ((env {})) + (pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(count, sub_atom(a, _, _, _, _), N)" env) + (pl-mk-trail)) + (pl-sa-num-val env "N")) + 3) + +;; -- aggregate_all/3 -- + +(pl-sa-test! + "aggregate_all count member [a,b,c] = 3" + (let + ((env {})) + (pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(count, member(_, [a,b,c]), N)" env) + (pl-mk-trail)) + (pl-sa-num-val env "N")) + 3) + +(pl-sa-test! + "aggregate_all count fail = 0" + (let + ((env {})) + (pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(count, fail, N)" env) + (pl-mk-trail)) + (pl-sa-num-val env "N")) + 0) + +(pl-sa-test! + "aggregate_all count always succeeds" + (pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(count, fail, _)" {}) + (pl-mk-trail)) + true) + +(define pl-sa-env-bag1 {}) +(pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(bag(X), member(X, [a,b,c]), L)" pl-sa-env-bag1) + (pl-mk-trail)) +(pl-sa-test! + "aggregate_all bag [a,b,c]" + (pl-sa-list-to-atoms (dict-get pl-sa-env-bag1 "L")) + (list "a" "b" "c")) + +(define pl-sa-env-bag2 {}) +(pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(bag(X), member(X, []), L)" pl-sa-env-bag2) + (pl-mk-trail)) +(pl-sa-test! + "aggregate_all bag empty goal = []" + (pl-sa-list-to-atoms (dict-get pl-sa-env-bag2 "L")) + (list)) + +(pl-sa-test! + "aggregate_all sum [1,2,3,4] = 10" + (let + ((env {})) + (pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(sum(X), member(X, [1,2,3,4]), S)" env) + (pl-mk-trail)) + (pl-sa-num-val env "S")) + 10) + +(pl-sa-test! + "aggregate_all max [3,1,4,1,5,9,2,6] = 9" + (let + ((env {})) + (pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(max(X), member(X, [3,1,4,1,5,9,2,6]), M)" env) + (pl-mk-trail)) + (pl-sa-num-val env "M")) + 9) + +(pl-sa-test! + "aggregate_all max empty fails" + (pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(max(X), member(X, []), M)" {}) + (pl-mk-trail)) + false) + +(pl-sa-test! + "aggregate_all min [3,1,4,1,5,9,2,6] = 1" + (let + ((env {})) + (pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(min(X), member(X, [3,1,4,1,5,9,2,6]), M)" env) + (pl-mk-trail)) + (pl-sa-num-val env "M")) + 1) + +(pl-sa-test! + "aggregate_all min empty fails" + (pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(min(X), member(X, []), M)" {}) + (pl-mk-trail)) + false) + +(define pl-sa-env-set1 {}) +(pl-solve-once! + pl-sa-db + (pl-sa-goal + "aggregate_all(set(X), member(X, [b,a,c,a,b]), S)" + pl-sa-env-set1) + (pl-mk-trail)) +(pl-sa-test! + "aggregate_all set [b,a,c,a,b] = [a,b,c]" + (pl-sa-list-to-atoms (dict-get pl-sa-env-set1 "S")) + (list "a" "b" "c")) + +(define pl-sa-env-set2 {}) +(pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(set(X), fail, S)" pl-sa-env-set2) + (pl-mk-trail)) +(pl-sa-test! + "aggregate_all set fail = []" + (pl-sa-list-to-atoms (dict-get pl-sa-env-set2 "S")) + (list)) + +(pl-sa-test! + "aggregate_all sum empty = 0" + (let + ((env {})) + (pl-solve-once! + pl-sa-db + (pl-sa-goal "aggregate_all(sum(X), fail, S)" env) + (pl-mk-trail)) + (pl-sa-num-val env "S")) + 0) + +(define pl-string-agg-tests-run! (fn () {:failed pl-sa-test-fail :passed pl-sa-test-pass :total pl-sa-test-count :failures pl-sa-test-failures}))