Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
24 KiB
24 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 now work —:-added to op table (prec 1200 xfx) with fix topl-token-opaccepting"op"token type. 15 tests intests/assert_rules.sx.findall/3,bagof/3,setof/3— sharedpl-collect-solutionsruns the goal in a fresh cut-box, deep-copies the template (viapl-deep-copywith var-map for shared-var preservation) on each success, returns false to backtrack, then restores trail.findallalways succeeds with a (possibly empty) list.bagoffails on empty.setofbuilds a string-keyed dict viapl-format-termfor sort+dedupe (viakeys+sort), fails on empty. Existential^deferred (operator). 11 tests intests/findall.sx.copy_term/2,functor/3,arg/3,=../2—copy_term/2reusespl-deep-copywith a fresh var-map (preserves source aliasing).functor/3handles 4 modes: compound→{name, arity}, atom→{atom, 0}, num→{num, 0}, var with ground name+arity→constructed term (pl-make-fresh-argsfor compound case).arg/3extracts 1-indexed arg from compound.=../2deferred — the tokenizer treats.as the clause terminator unconditionally, so=..lexes as=+.+.; needs special-case lex (or surface syntax via a different name). 14 tests intests/term_inspect.sx.- String/atom predicates
Phase 5 — Hyperscript integration
prolog-queryprimitive callable from SX/Hyperscript- Hyperscript DSL:
when allowed(user, :edit) then …← blocked (needslib/hyperscript/**, out of scope) - 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 —
sub_atom/5(non-deterministic substring enumeration; CPS loop over all (start,sublen) pairs; trail-undo only on backtrack) +aggregate_all/3(6 templates: count/bag/sum/max/min/set; usespl-collect-solutions). 25 tests intests/string_agg.sx. Total 496 (+25). - 2026-04-25 —
:-operator + assert with rules: added(list ":-" 1200 "xfx")topl-op-table; fixedpl-token-opto accept"op"token type (tokenizer emits:-as"op", not"atom").pl-build-clausealready handled("compound" ":-" ...).assert((head :- body))now works for facts+rules. 15 tests intests/assert_rules.sx. Total 471 (+15). - 2026-04-25 — IO/term predicates:
term_to_atom/2(bidirectional: format term or parse atom),term_string/2(alias),with_output_to/2(atom/string sinks — saves/restorespl-output-buffer),writeln/1,format/1(~n/~t/~~),format/2(~w/~a/~d pull from arg list). 24 tests intests/io_predicates.sx. Total 456 (+24). - 2026-04-25 — Char predicates:
char_type/2(9 modes: alpha/alnum/digit/digit(N)/space/white/upper(L)/lower(U)/ascii(C)/punct),upcase_atom/2,downcase_atom/2,string_upper/2,string_lower/2. 10 helpers usingchar-code/char-from-codeSX primitives. 27 tests intests/char_predicates.sx. Total 432 (+27). - 2026-04-25 — Set/fold predicates:
foldl/4(CPS fold-left, threads accumulator viapl-apply-goal),list_to_set/2(dedup preserving first-occurrence),intersection/3,subtract/3,union/3(all viapl-struct-eq?). 3 new helpers, 15 tests intests/set_predicates.sx. Total 405 (+15). - 2026-04-25 — Meta-call predicates:
forall/2(negation-of-counterexample),maplist/2(goal over list),maplist/3(map goal building output list),include/3(filter by goal success),exclude/3(filter by goal failure). Newpl-apply-goalhelper extends a goal with extra args. 15 tests intests/meta_call.sx. Total 390 (+15). - 2026-04-25 — List/utility predicates:
==/2,\==/2(structural equality/inequality viapl-struct-eq?),flatten/2(deep Prolog-list flatten),numlist/3(integer range list),atomic_list_concat/2(join with no sep),atomic_list_concat/3(join with separator),sum_list/2,max_list/2,min_list/2(arithmetic folds),delete/3(remove all struct-equal elements). 7 new helpers, 33 tests intests/list_predicates.sx. Total 375 (+33). - 2026-04-25 — Meta/logic predicates:
\+/1(negation-as-failure, trail-undo on success),not/1(alias),once/1(commit to first solution via if-then-else),ignore/1(always succeed),ground/1(all vars bound),sort/2(sort + dedup by formatted key),msort/2(sort, keep dups),atom_number/2(bidirectional),number_string/2(bidirectional). 2 helpers (pl-ground?,pl-sort-pairs-dedup). 25 tests intests/meta_predicates.sx. Total 342 (+25). - 2026-04-25 — ISO utility predicates batch:
succ/2(bidirectional),plus/3(3-mode bidirectional),between/3(backtracking range generator),length/2(bidirectional list length + var-list constructor),last/2,nth0/3,nth1/3,max/2+min/2in arithmetic eval. 6 new helper functions (pl-list-length,pl-make-list-of-vars,pl-between-loop!,pl-solve-between!,pl-solve-last!,pl-solve-nth0!). 29 tests intests/iso_predicates.sx. Phase 6 complete: scoreboard already at 317, far above 200+ target. Hyperscript DSL blocked (needslib/hyperscript/**). Total 317 (+29). - 2026-04-25 —
prolog-querySX API (lib/prolog/query.sx). New public API layer:pl-load source-str → db,pl-query-all db query-str → list of solution dicts,pl-query-one db query-str → dict or nil,pl-query src query → list(convenience). Each solution dict maps variable name strings to their formatted term strings. Var names extracted from pre-instantiation parse AST. Trail is marked before solve and reset after to ensure clean state. 16 tests intests/query_api.sxcover fact lookup, no-solution, boolean queries, multi-var, recursive rules, is/2 built-in, query-one, convenience form. Total 288 (+16). - 2026-04-25 — String/atom predicates. Type-test predicates:
var/1,nonvar/1,atom/1,number/1,integer/1,float/1(always-fail),compound/1,callable/1,atomic/1,is_list/1. String/atom operations:atom_length/2,atom_concat/3(3 modes: both-ground, result+first, result+second),atom_chars/2(bidirectional),atom_codes/2(bidirectional),char_code/2(bidirectional),number_codes/2,number_chars/2. 7 helper functions in runtime.sx (pl-list-to-prolog,pl-proper-list?,pl-prolog-list-to-sx,pl-solve-atom-concat!,pl-solve-atom-chars!,pl-solve-atom-codes!,pl-solve-char-code!). 34 tests intests/atoms.sx. Total 272 (+34). - 2026-04-25 —
copy_term/2+functor/3+arg/3(term inspection).copy_termis a one-line dispatch to existingpl-deep-copy.functor/3is 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 viapl-make-fresh-args; arity 0 → atom/num).arg/3extracts 1-indexed arg with bounds-fail. New helperpl-solve-eq2!for paired-unification with shared trail-undo. 14 tests intests/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 collectorpl-collect-solutionsruns the goal in a fresh cut-box, deep-copies the template per success (pl-deep-copywalks term, allocates fresh runtime vars via shared var-map so co-occurrences keep aliasing), returns false to keep backtracking, thenpl-trail-undo-to!to clean up.findallalways builds a list.bagoffails on empty.setofuses apl-format-term-keyed dict + SXsortfor dedupe + ordering. Newtests/findall.sx11 tests. Total 224 (+11). Existential^deferred — needs operator. - 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.
- Phase 5 Hyperscript DSL —
lib/hyperscript/**is out of scope for this loop. Needslib/hyperscript/parser.sx+ evaluator to addwhen allowed(user, :edit) then …syntax. Skipping; Phase 5 item 1 (prolog-querySX API) is done.