diff --git a/lib/prolog/conformance.sh b/lib/prolog/conformance.sh index 4376638c..04eb86ac 100755 --- a/lib/prolog/conformance.sh +++ b/lib/prolog/conformance.sh @@ -43,6 +43,7 @@ SUITES=( "advanced:lib/prolog/tests/advanced.sx:pl-advanced-tests-run!" "compiler:lib/prolog/tests/compiler.sx:pl-compiler-tests-run!" "cross_validate:lib/prolog/tests/cross_validate.sx:pl-cross-validate-tests-run!" + "integration:lib/prolog/tests/integration.sx:pl-integration-tests-run!" ) SCRIPT='(epoch 1) diff --git a/lib/prolog/scoreboard.json b/lib/prolog/scoreboard.json index 49c6c7c4..97fc3716 100644 --- a/lib/prolog/scoreboard.json +++ b/lib/prolog/scoreboard.json @@ -1,7 +1,7 @@ { - "total_passed": 517, + "total_passed": 571, "total_failed": 0, - "total": 517, - "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},"advanced":{"passed":21,"total":21,"failed":0}}, - "generated": "2026-04-25T14:12:52+00:00" + "total": 571, + "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},"advanced":{"passed":21,"total":21,"failed":0},"compiler":{"passed":17,"total":17,"failed":0},"cross_validate":{"passed":17,"total":17,"failed":0},"integration":{"passed":20,"total":20,"failed":0}}, + "generated": "2026-05-05T20:36:53+00:00" } diff --git a/lib/prolog/scoreboard.md b/lib/prolog/scoreboard.md index e0570b16..5dcb7d45 100644 --- a/lib/prolog/scoreboard.md +++ b/lib/prolog/scoreboard.md @@ -1,7 +1,7 @@ # Prolog scoreboard -**517 / 517 passing** (0 failure(s)). -Generated 2026-04-25T14:12:52+00:00. +**571 / 571 passing** (0 failure(s)). +Generated 2026-05-05T20:36:53+00:00. | Suite | Passed | Total | Status | |-------|--------|-------|--------| @@ -30,6 +30,9 @@ Generated 2026-04-25T14:12:52+00:00. | assert_rules | 15 | 15 | ok | | string_agg | 25 | 25 | ok | | advanced | 21 | 21 | ok | +| compiler | 17 | 17 | ok | +| cross_validate | 17 | 17 | ok | +| integration | 20 | 20 | 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/integration.sx b/lib/prolog/tests/integration.sx new file mode 100644 index 00000000..6c2428ff --- /dev/null +++ b/lib/prolog/tests/integration.sx @@ -0,0 +1,172 @@ +;; lib/prolog/tests/integration.sx — end-to-end integration tests via pl-query-* API +;; +;; Tests the full source→parse→load→solve pipeline with real programs. +;; Covers: permission system, graph reachability, quicksort, fibonacci, dynamic KB. + +(define pl-int-test-count 0) +(define pl-int-test-pass 0) +(define pl-int-test-fail 0) +(define pl-int-test-failures (list)) + +(define + pl-int-test! + (fn + (name got expected) + (begin + (set! pl-int-test-count (+ pl-int-test-count 1)) + (if + (= got expected) + (set! pl-int-test-pass (+ pl-int-test-pass 1)) + (begin + (set! pl-int-test-fail (+ pl-int-test-fail 1)) + (append! + pl-int-test-failures + (str name "\n expected: " expected "\n got: " got))))))) + +;; ── Permission system ── +;; role/2 + permission/2 facts, allowed/2 rule + +(define + pl-int-perm-src + "role(alice, admin). role(bob, editor). role(charlie, viewer). permission(admin, read). permission(admin, write). permission(admin, delete). permission(editor, read). permission(editor, write). permission(viewer, read). allowed(U, A) :- role(U, R), permission(R, A).") + +(define pl-int-perm-db (pl-load pl-int-perm-src)) + +(pl-int-test! + "alice can read" + (len (pl-query-all pl-int-perm-db "allowed(alice, read)")) + 1) + +(pl-int-test! + "alice can delete" + (len (pl-query-all pl-int-perm-db "allowed(alice, delete)")) + 1) + +(pl-int-test! + "charlie cannot write" + (len (pl-query-all pl-int-perm-db "allowed(charlie, write)")) + 0) + +(pl-int-test! + "alice has 3 permissions" + (len (pl-query-all pl-int-perm-db "allowed(alice, A)")) + 3) + +(pl-int-test! + "only one user can delete" + (len (pl-query-all pl-int-perm-db "allowed(U, delete)")) + 1) + +(pl-int-test! + "the deleter is alice" + (dict-get (first (pl-query-all pl-int-perm-db "allowed(U, delete)")) "U") + "alice") + +;; ── Graph reachability ── +;; Directed edges; path/2 transitive closure via two clauses + +(define + pl-int-graph-src + "edge(a, b). edge(b, c). edge(c, d). edge(b, d). path(X, Y) :- edge(X, Y). path(X, Y) :- edge(X, Z), path(Z, Y).") + +(define pl-int-graph-db (pl-load pl-int-graph-src)) + +(pl-int-test! + "direct edge a→b is a path" + (len (pl-query-all pl-int-graph-db "path(a, b)")) + 1) + +(pl-int-test! + "transitive path a→c" + (len (pl-query-all pl-int-graph-db "path(a, c)")) + 1) + +(pl-int-test! + "no path d→a (no back-edges)" + (len (pl-query-all pl-int-graph-db "path(d, a)")) + 0) + +(pl-int-test! + "4 derivations from a (b,c,d via two routes to d)" + (len (pl-query-all pl-int-graph-db "path(a, Y)")) + 4) + +;; ── Quicksort ── +;; Partition-and-recurse; uses its own append/3 to avoid DB pollution + +(define + pl-int-qs-src + "partition(_, [], [], []). partition(Piv, [H|T], [H|Less], Greater) :- H =< Piv, !, partition(Piv, T, Less, Greater). partition(Piv, [H|T], Less, [H|Greater]) :- partition(Piv, T, Less, Greater). append([], L, L). append([H|T], L, [H|R]) :- append(T, L, R). quicksort([], []). quicksort([H|T], Sorted) :- partition(H, T, Less, Greater), quicksort(Less, SL), quicksort(Greater, SG), append(SL, [H|SG], Sorted).") + +(define pl-int-qs-db (pl-load pl-int-qs-src)) + +(pl-int-test! + "quicksort([]) = [] (ground check)" + (len (pl-query-all pl-int-qs-db "quicksort([], [])")) + 1) + +(pl-int-test! + "quicksort([3,1,2]) = [1,2,3] (ground check)" + (len (pl-query-all pl-int-qs-db "quicksort([3,1,2], [1,2,3])")) + 1) + +(pl-int-test! + "quicksort([5,3,1,4,2]) = [1,2,3,4,5] (ground check)" + (len (pl-query-all pl-int-qs-db "quicksort([5,3,1,4,2], [1,2,3,4,5])")) + 1) + +(pl-int-test! + "quicksort([3,1,2], [3,1,2]) fails — unsorted order rejected" + (len (pl-query-all pl-int-qs-db "quicksort([3,1,2], [3,1,2])")) + 0) + +;; ── Fibonacci ── +;; Naive recursive; ground checks avoid list-format uncertainty + +(define + pl-int-fib-src + "fib(0, 0). fib(1, 1). fib(N, F) :- N > 1, N1 is N - 1, N2 is N - 2, fib(N1, F1), fib(N2, F2), F is F1 + F2.") + +(define pl-int-fib-db (pl-load pl-int-fib-src)) + +(pl-int-test! + "fib(0, 0) succeeds" + (len (pl-query-all pl-int-fib-db "fib(0, 0)")) + 1) + +(pl-int-test! + "fib(5, 5) succeeds" + (len (pl-query-all pl-int-fib-db "fib(5, 5)")) + 1) + +(pl-int-test! + "fib(7, 13) succeeds" + (len (pl-query-all pl-int-fib-db "fib(7, 13)")) + 1) + +;; ── Dynamic knowledge base ── +;; Assert and retract facts; the DB dict is mutable so mutations persist + +(define pl-int-dyn-src "color(red). color(green). color(blue).") +(define pl-int-dyn-db (pl-load pl-int-dyn-src)) + +(pl-int-test! + "initial KB: 3 colors" + (len (pl-query-all pl-int-dyn-db "color(X)")) + 3) + +(pl-int-test! + "after assert(color(yellow)): 4 colors" + (begin + (pl-query-all pl-int-dyn-db "assert(color(yellow))") + (len (pl-query-all pl-int-dyn-db "color(X)"))) + 4) + +(pl-int-test! + "after retract(color(red)): back to 3 colors" + (begin + (pl-query-all pl-int-dyn-db "retract(color(red))") + (len (pl-query-all pl-int-dyn-db "color(X)"))) + 3) + +(define pl-integration-tests-run! (fn () {:failed pl-int-test-fail :passed pl-int-test-pass :total pl-int-test-count :failures pl-int-test-failures})) diff --git a/plans/prolog-on-sx.md b/plans/prolog-on-sx.md index 06f029b5..d20bc338 100644 --- a/plans/prolog-on-sx.md +++ b/plans/prolog-on-sx.md @@ -74,7 +74,7 @@ Representation choices (finalise in phase 1, document here): ### Phase 5 — Hyperscript integration - [x] `prolog-query` primitive callable from SX/Hyperscript - [ ] Hyperscript DSL: `when allowed(user, :edit) then …` ← **blocked** (needs `lib/hyperscript/**`, out of scope) -- [ ] Integration suite +- [x] Integration suite ### Phase 6 — ISO conformance - [x] Vendor Hirst's conformance tests @@ -88,6 +88,7 @@ Representation choices (finalise in phase 1, document here): _Newest first. Agent appends on every commit._ +- 2026-05-05 — Integration test suite (`tests/integration.sx`): 20 end-to-end tests via `pl-query-*` API covering permission system (6), graph reachability (4), quicksort (4), fibonacci (3), dynamic KB (3). Suite added to conformance harness. Total **571** (+20). - 2026-04-25 — `pl-compiled-matches-interp?` cross-validator in `compiler.sx`: loads source into both a plain and a compiled DB, runs the same goal, returns true iff solution counts match. `tests/cross_validate.sx` applies this to 17 goals across append/member/ancestor/cut/arithmetic/if-then-else, locking the interpreter as the reference against which any future compiler change must agree. Total **551** (+17). - 2026-04-25 — Clause compiler (`lib/prolog/compiler.sx`): `pl-compile-clause` converts parse-AST clauses to SX closures `(fn (goal trail db cut-box k) bool)`. Pre-collects var names at compile time; `pl-cmp-build-term` reconstructs fresh runtime terms per call. `pl-compile-db!` compiles all clauses in a DB and stores them in `:compiled` table. `pl-solve-user!` in runtime.sx auto-dispatches to compiled lambdas when present, falls back to interpreted. `pl-try-compiled-clauses!` mirrors `pl-try-clauses!` cut semantics. 17 tests in `tests/compiler.sx`. Total **534** (+17). - 2026-04-25 — `predsort/3` (insertion-sort with 3-arg comparator predicate, deduplicates `=` pairs), `term_variables/2` (collect unbound vars left-to-right, dedup by id), arithmetic extensions (`floor/1`, `ceiling/1`, `truncate/1`, `round/1`, `sign/1`, `sqrt/1`, `pow/2`, `**/2`, `^/2`, `integer/1`, `float/1`, `float_integer_part/1`, `float_fractional_part/1`). 21 tests in `tests/advanced.sx`. Total **517** (+21).