diff --git a/lib/prolog/conformance.sh b/lib/prolog/conformance.sh new file mode 100755 index 00000000..4dd949c8 --- /dev/null +++ b/lib/prolog/conformance.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Run every Prolog test suite via sx_server and refresh scoreboard.{json,md}. +# Exit 0 if all green, 1 if any failures. +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/../.." && pwd)" +SX="${SX_SERVER:-/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe}" + +if [[ ! -x "$SX" ]]; then + echo "sx_server not found at $SX (set SX_SERVER env to override)" >&2 + exit 2 +fi + +cd "$ROOT" + +# name : test-file : runner-fn +SUITES=( + "parse:lib/prolog/tests/parse.sx:pl-parse-tests-run!" + "unify:lib/prolog/tests/unify.sx:pl-unify-tests-run!" + "clausedb:lib/prolog/tests/clausedb.sx:pl-clausedb-tests-run!" + "solve:lib/prolog/tests/solve.sx:pl-solve-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!" + "nqueens:lib/prolog/tests/programs/nqueens.sx:pl-nqueens-tests-run!" + "family:lib/prolog/tests/programs/family.sx:pl-family-tests-run!" +) + +SCRIPT='(epoch 1) +(load "lib/prolog/tokenizer.sx") +(load "lib/prolog/parser.sx") +(load "lib/prolog/runtime.sx")' +for entry in "${SUITES[@]}"; do + IFS=: read -r _ file _ <<< "$entry" + SCRIPT+=$'\n(load "'"$file"$'")' +done +for entry in "${SUITES[@]}"; do + IFS=: read -r _ _ fn <<< "$entry" + SCRIPT+=$'\n(eval "('"$fn"$')")' +done + +OUTPUT="$(printf '%s\n' "$SCRIPT" | "$SX" 2>&1)" + +mapfile -t LINES < <(printf '%s\n' "$OUTPUT" | grep -E '^\{:failed') + +if [[ ${#LINES[@]} -ne ${#SUITES[@]} ]]; then + echo "Expected ${#SUITES[@]} suite results, got ${#LINES[@]}" >&2 + echo "---- raw output ----" >&2 + printf '%s\n' "$OUTPUT" >&2 + exit 3 +fi + +TOTAL_PASS=0 +TOTAL_FAIL=0 +TOTAL=0 +JSON_SUITES="" +MD_ROWS="" + +for i in "${!SUITES[@]}"; do + IFS=: read -r name _ _ <<< "${SUITES[$i]}" + line="${LINES[$i]}" + passed=$(grep -oE ':passed [0-9]+' <<< "$line" | grep -oE '[0-9]+') + total=$(grep -oE ':total [0-9]+' <<< "$line" | grep -oE '[0-9]+') + failed=$(grep -oE ':failed [0-9]+' <<< "$line" | grep -oE '[0-9]+') + TOTAL_PASS=$((TOTAL_PASS + passed)) + TOTAL_FAIL=$((TOTAL_FAIL + failed)) + TOTAL=$((TOTAL + total)) + status="ok" + [[ "$failed" -gt 0 ]] && status="FAIL" + [[ -n "$JSON_SUITES" ]] && JSON_SUITES+="," + JSON_SUITES+="\"$name\":{\"passed\":$passed,\"total\":$total,\"failed\":$failed}" + MD_ROWS+="| $name | $passed | $total | $status |"$'\n' +done + +WHEN="$(date -Iseconds 2>/dev/null || date)" + +cat > "$HERE/scoreboard.json" < "$HERE/scoreboard.md" <&2 + exit 1 +fi + +echo "All $TOTAL tests pass." diff --git a/lib/prolog/scoreboard.json b/lib/prolog/scoreboard.json new file mode 100644 index 00000000..44d60788 --- /dev/null +++ b/lib/prolog/scoreboard.json @@ -0,0 +1,7 @@ +{ + "total_passed": 183, + "total_failed": 0, + "total": 183, + "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},"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-25T06:19:36+00:00" +} diff --git a/lib/prolog/scoreboard.md b/lib/prolog/scoreboard.md new file mode 100644 index 00000000..8d21270b --- /dev/null +++ b/lib/prolog/scoreboard.md @@ -0,0 +1,19 @@ +# Prolog scoreboard + +**183 / 183 passing** (0 failure(s)). +Generated 2026-04-25T06:19:36+00:00. + +| Suite | Passed | Total | Status | +|-------|--------|-------|--------| +| parse | 25 | 25 | ok | +| unify | 47 | 47 | ok | +| clausedb | 14 | 14 | ok | +| solve | 62 | 62 | ok | +| append | 6 | 6 | ok | +| reverse | 6 | 6 | ok | +| member | 7 | 7 | ok | +| nqueens | 6 | 6 | ok | +| family | 10 | 10 | ok | + +Run `bash lib/prolog/conformance.sh` to refresh. Override the binary +with `SX_SERVER=path/to/sx_server.exe bash …`. diff --git a/plans/prolog-on-sx.md b/plans/prolog-on-sx.md index faadb9e0..19610a0e 100644 --- a/plans/prolog-on-sx.md +++ b/plans/prolog-on-sx.md @@ -61,7 +61,7 @@ Representation choices (finalise in phase 1, document here): - [x] `member.pl` — generate all solutions via backtracking — `lib/prolog/tests/programs/member.{pl,sx}`. Classic 2-clause `member(X, [X|_])` + `member(X, [_|T]) :- member(X, T)`. 7 tests cover bound-element hit/miss, empty list, generator (count = list length), first-solution binding, duplicate matches counted twice, anonymous head-cell unification. - [x] `nqueens.pl` — 8-queens — `lib/prolog/tests/programs/nqueens.{pl,sx}`. Permute-and-test formulation: `queens(L, Qs) :- permute(L, Qs), safe(Qs)` + `select` + `safe` + `no_attack`. Tested at N=1 (1), N=2 (0), N=3 (0), N=4 (2), N=5 (10) plus first-solution check at N=4 = `[2, 4, 1, 3]`. N=8 omitted — interpreter is too slow (40320 perms); add once compiled clauses or constraint-style placement land. `range/3` skipped pending arithmetic-comparison built-ins (`>/2` etc.). - [x] `family.pl` — facts + rules (parent/ancestor) — `lib/prolog/tests/programs/family.{pl,sx}`. 5 parent facts + male/female + derived `father`/`mother`/`ancestor`/`sibling`. 10 tests cover direct facts, fact count, transitive ancestor through 3 generations, descendant counting, gender-restricted father/mother, sibling via shared parent + `\=`. -- [ ] `lib/prolog/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md` +- [x] `lib/prolog/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md` — bash script feeds load + eval epoch script to sx_server, parses each suite's `{:failed N :passed N :total N :failures (...)}` line, writes JSON (machine) + MD (human) scoreboards. Exit non-zero on any failure. `SX_SERVER` env var overrides binary path. First scoreboard: 183 / 183. - [x] Target: all 5 classic programs passing — append (6) + reverse (6) + member (7) + nqueens (6) + family (10) = 35 program tests, all green. Phase 3 architecturally complete bar the conformance harness/scoreboard. ### Phase 4 — operator table + more built-ins (next run) @@ -88,6 +88,7 @@ Representation choices (finalise in phase 1, document here): _Newest first. Agent appends on every commit._ +- 2026-04-25 — Conformance harness landed. `lib/prolog/conformance.sh` runs all 9 suites in one sx_server epoch, parses the `{:failed/:passed/:total/:failures}` summary lines, and writes `scoreboard.json` + `scoreboard.md`. `SX_SERVER` env var overrides the binary path; default points at the main-repo build. Phase 3 fully complete: 183 / 183 passing across parse/unify/clausedb/solve/append/reverse/member/nqueens/family. - 2026-04-25 — `family.pl` fifth classic program — completes the 5-program target. 5-fact pedigree + male/female + derived father/mother/ancestor/sibling. 10 tests cover fact lookup + count, transitive ancestor through 3 generations, descendant counting (5), gender-restricted derivations, sibling via shared parent guarded by `\=`. Total 183 (+10). All 5 classic programs ticked; Phase 3 needs only conformance harness + scoreboard left. - 2026-04-25 — `nqueens.pl` fourth classic program. Permute-and-test variant exercises every Phase-3 feature: lists with `[H|T]` cons sugar, multi-clause backtracking, recursive `permute`/`select`/`safe`/`no_attack`, `is/2` arithmetic on diagonals, `\=/2` for diagonal-conflict check. 6 tests at N ∈ {1,2,3,4,5} with expected counts {1,0,0,2,10} + first-solution `[2,4,1,3]`. N=5 takes ~30s (120 perms × safe-check); N=8 omitted as it would be ~thousands of seconds. Total 173 (+6). - 2026-04-25 — `member.pl` third classic program. Standard 2-clause definition; 7 tests cover bound-element hit/miss, empty-list fail, generator-count = list length, first-solution binding (X=11), duplicate elements matched twice on backtrack, anonymous-head unification (`member(a, [X, b, c])` binds X=a). Total 167 (+7).