Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
16 KiB
16 KiB
Prolog-on-SX: mini-Prolog interpreter on delimited continuations
Horn clauses + unification + cut + arithmetic, implemented as an interpreter (clauses live as SX data, a SX-implemented solver walks them). Backtracking is powered by the delimited-continuations machinery in lib/callcc.sx + spec/evaluator.sx Step 5 — this is the reason Prolog fits SX well.
End-state goal: 200+ tests passing (classic programs + Hirst's ISO conformance subset + Hyperscript integration suite). Long-lived background agent driving the scoreboard up.
Ground rules
- Scope: only touch
lib/prolog/**andplans/prolog-on-sx.md. Do not editspec/,hosts/,shared/,lib/js/**,lib/hyperscript/**,lib/lua/**,lib/stdlib.sx, or anything inlib/root. Prolog primitives go inlib/prolog/runtime.sx. - Shared-file issues go under "Blockers" below with a minimal repro; do not fix from this loop.
- SX files: use
sx-treeMCP tools only. - Architecture: Prolog source → term AST → clause DB. Solver is SX code walking the DB; backtracking via delimited continuations, not a separate trail machine.
- Commits: one feature per commit. Keep
## Progress logupdated and tick boxes.
Architecture sketch
Prolog source text
│
▼
lib/prolog/tokenizer.sx — atoms, vars, numbers, punct, comments
│
▼
lib/prolog/parser.sx — term AST; phase 1: f(a,b) syntax only, no operator table
│
▼
lib/prolog/runtime.sx — clause DB, unify!, trail, solver (DFS + delimited-cont backtracking)
│ built-ins: =/2, \=/2, !/0, is/2, call/1, findall/3, …
▼
solutions / side-effects
Representation choices (finalise in phase 1, document here):
- Term: nested SX list. Compound
(functor arg1 arg2). Atom = symbol. Number = number. Variable ={:var "X" :binding <ref>}with mutable binding slot. - List: cons-cell compound
(. H T)or similar.[1,2,3]sugar desugared at parse. - Clause:
{:head <term> :body <term>}where body is the conjunction goal. - Clause DB: dict
"functor/arity" → list of clauses.
Roadmap
Phase 1 — tokenizer + term parser (no operator table)
- Tokenizer: atoms (lowercase/quoted), variables (uppercase/
_), numbers, strings, punct( ) , . [ ] | ! :-, comments (%,/* */) - Parser: clauses
head :- body.and factshead.; termsatom | Var | number | compound(args) | [list,sugar] - Skip for phase 1: operator table.
X is Y + 1must be writtenis(X, '+'(Y, 1));=written=(X, Y). Operators land in phase 4. - Unit tests in
lib/prolog/tests/parse.sx— 25 pass
Phase 2 — unification + trail
make-var,walk(follow binding chain),prolog-unify!(terms + trail → bool),trail-undo-to!- Occurs-check off by default, exposed as flag
- 30+ unification tests in
lib/prolog/tests/unify.sx: atoms, vars, compounds, lists, cyclic (no-occurs-check), mutual occurs — 47 pass
Phase 3 — clause DB + DFS solver + cut + first classic programs
- 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 intests/clausedb.sx - 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. - Cut (
!): cut barrier at current choice-point frame; collapse all up to barrier — two-cut-box scheme: eachpl-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,->/2inside;,call/1,write/1,nl/0— all 11 done.write/1andnl/0use a globalpl-output-bufferstring +pl-output-clear!for testability;pl-format-termwalks deep then renders atoms/nums/strs/compounds/vars (var →_<id>). 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/2with+ - * / mod abs—pl-eval-arithwalks deep, recurses on compounds, dispatches on functor; binary+ - * / mod, binary AND unary-, unaryabs.is/2evaluates RHS, wraps as("num" v), unifies viapl-solve-eq!. 11 tests cover each op + nested + ground LHS match/mismatch + bound-var-on-RHS chain. - Classic programs in
lib/prolog/tests/programs/:append.pl— list append (with backtracking) —lib/prolog/tests/programs/append.{pl,sx}. 6 tests cover: build (append([], L, X),append([1,2], [3,4], X)), check ground match/mismatch, full split-backtracking (append(X, Y, [1,2,3])→ 4 solutions), single-deduce (append(X, [3], [1,2,3])→ X=[1,2]).reverse.pl— naive reverse —lib/prolog/tests/programs/reverse.{pl,sx}. Naive reverse via append:reverse([H|T], R) :- reverse(T, RT), append(RT, [H], R). 6 tests cover empty, singleton, 3-list, 4-atom-list, ground match, ground mismatch.member.pl— generate all solutions via backtracking —lib/prolog/tests/programs/member.{pl,sx}. Classic 2-clausemember(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.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/3skipped pending arithmetic-comparison built-ins (>/2etc.).family.pl— facts + rules (parent/ancestor) —lib/prolog/tests/programs/family.{pl,sx}. 5 parent facts + male/female + derivedfather/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— 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_SERVERenv var overrides binary path. First scoreboard: 183 / 183.- 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)
- Operator table parsing (prefix/infix/postfix, precedence, assoc) —
pl-op-table(15 entries:, ; -> = \= is < > =< >= + - * / mod); precedence-climbing parser viapp-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-5still tokenises as bare atom + num as before). Comparison built-ins</2 >/2 =</2 >=/2added. Newtests/operators.sx19 tests cover assoc/precedence/parens + solver via infix. assert/1,asserta/1,assertz/1,retract/1—assertaliasesassertz. Helperspl-rt-to-ast(deep-walk + replace runtime vars with_G<id>parse markers) +pl-build-clause(detect:-head).assertzusespl-db-add!;assertauses newpl-db-prepend!.retractwalks goal, looks up by functor/arity, tries each clause via unification, removes first match by index (pl-list-without). 11 tests intests/dynamic.sx. Rule-asserts deferred —:-not in op table yet, so only fact-shaped clauses for now.findall/3,bagof/3,setof/3copy_term/2,functor/3,arg/3,=../2- String/atom predicates
Phase 5 — Hyperscript integration
prolog-queryprimitive callable from SX/Hyperscript- Hyperscript DSL:
when allowed(user, :edit) then … - Integration suite
Phase 6 — ISO conformance
- Vendor Hirst's conformance tests
- Drive scoreboard to 200+
Phase 7 — compiler (later, optional)
- Compile clauses to SX continuations for speed
- Keep interpreter as the reference
Progress log
Newest first. Agent appends on every commit.
- 2026-04-25 — Dynamic clauses:
assert/1,assertz/1,asserta/1,retract/1. New helperspl-rt-to-ast(deep-walk runtime term → parse-AST, mapping unbound runtime vars to_G<id>markers sopl-instantiate-freshproduces fresh vars per call) +pl-build-clause+pl-db-prepend!+pl-list-without.retractkeeps runtime vars (so the caller's vars get bound), walks head for the functor/arity key, tries each stored clause viapl-unify!, removes the first match by index. 11 tests intests/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 =</2 >=/2to runtime (eval both sides, compare). Newtests/operators.sx19 tests; conformance script gained an operators row. Total 202 (+19). Prefix/postfix deferred —-5keeps old bare-atom semantics. - 2026-04-25 — Conformance harness landed.
lib/prolog/conformance.shruns all 9 suites in one sx_server epoch, parses the{:failed/:passed/:total/:failures}summary lines, and writesscoreboard.json+scoreboard.md.SX_SERVERenv 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.plfifth 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.plfourth classic program. Permute-and-test variant exercises every Phase-3 feature: lists with[H|T]cons sugar, multi-clause backtracking, recursivepermute/select/safe/no_attack,is/2arithmetic on diagonals,\=/2for 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.plthird 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). - 2026-04-25 —
reverse.plsecond classic program. Naive reverse defined via append. 6 tests (empty/singleton/3-list/4-atom-list/ground match/ground mismatch). Confirms the solver handles non-trivial recursive composition:reverse([1,2,3], R)recurses to depth 3 then unwinds via 3 nestedappends. Total 160 (+6). - 2026-04-25 —
append.plfirst classic program.lib/prolog/tests/programs/append.plis the canonical 2-clause source;append.sxembeds the source as a string (no file-read primitive in SX yet) and runs 6 tests covering build, check, full split-backtrack (4 solutions), and deduction modes. Helperspl-ap-list-to-sx/pl-ap-term-to-sxconvert deep-walked Prolog lists (("compound" "." (h t))/("atom" "[]")) to SX lists for structural assertion. Total 154 (+6). - 2026-04-25 —
is/2arithmetic landed.pl-eval-arithrecursively evaluates ground RHS expressions (binary+ - * /,mod; binary+unary-; unaryabs);is/2wraps the value as("num" v)and unifies viapl-solve-eq!, so it works in all three modes — bind unbound LHS, check ground LHS for equality, propagate from earlier var bindings on RHS. 11 tests, total 148 (+11). Without operator support, expressions must be written prefix:is(X, +(2, *(3, 4))). - 2026-04-25 —
write/1+nl/0landed using global string buffer (pl-output-buffer+pl-output-clear!+pl-output-write!).pl-format-termwalks deep + dispatches on atom/num/str/compound/var;pl-format-argsrecursively comma-joins. 7 new tests cover atom/num/compound formatting, conjunction order, var-walk, andnl. 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 —
->/2if-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/1landed.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/1opens a fresh inner cut-box (ISO opacity: cut insidecall(G)commits G, not caller). 11 new tests intests/solve.sxcover atoms+vars for\=, both branches + count for;, andcall/1against atoms / compounds / bound goal vars. Total 121 (+11). Box not yet ticked —->/2,write/1,nl/0still pending. - 2026-04-25 — Cut (
!/0) landed.pl-cut?predicate; solver functions all take acut-box;pl-solve-user!creates a fresh inner-cut-box and snapshotsouter-was-cut;pl-try-clauses!abandons alternatives when inner.cut OR (outer.cut transitioned false→true during this call). 6 new cut tests intests/solve.sxcovering bare cut, clause-commit, choice-commit, cut+fail blocks alt clauses, post-cut goal backtracks freely, inner cut isolation. Total 110 (+6). - 2026-04-25 — Phase 3 DFS solver landed (CPS, trail-based backtracking; delimited conts deferred).
pl-solve!+pl-solve-eq!+pl-solve-user!+pl-try-clauses!+pl-solve-once!+pl-solve-count!in runtime.sx. Built-ins:true/0,fail/0,=/2,,/2. Newtests/solve.sx18/18 green covers atomic goals, =, conjunction, fact lookup, multi-solution count, recursive ancestor rule, trail-undo verification. Bug fix:pl-instantiatehad no("clause" h b)case → vars in rule head/body were never instantiated, so rule resolution silently failed against runtime-var goals. Added clause case to recurse with shared var-env. Total 104 (+18). - 2026-04-24 — Phase 3 clause DB landed:
pl-mk-db+pl-head-key/pl-clause-key/pl-goal-key+pl-db-add!/pl-db-load!/pl-db-lookup/pl-db-lookup-goalin runtime.sx. Newtests/clausedb.sx14/14 green. Total 86 (+14). Loader preserves declaration order (append!). - 2026-04-24 — Verified phase 1+2 already implemented on loops/prolog:
pl-parse-tests-run!25/25,pl-unify-tests-run!47/47 (72 total). Ticked phase 1+2 boxes. - (awaiting phase 1)
Blockers
Shared-file issues that need someone else to fix. Minimal repro only.
- (none yet)