Files
rose-ash/plans/prolog-on-sx.md
giles 93b31b6c8a
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
prolog: reverse.pl + reverse.sx (naive via append), 6 tests
2026-04-25 04:26:20 +00:00

11 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/** and plans/prolog-on-sx.md. Do not edit spec/, hosts/, shared/, lib/js/**, lib/hyperscript/**, lib/lua/**, lib/stdlib.sx, or anything in lib/ root. Prolog primitives go in lib/prolog/runtime.sx.
  • Shared-file issues go under "Blockers" below with a minimal repro; do not fix from this loop.
  • SX files: use sx-tree MCP 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 log updated 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 facts head.; terms atom | Var | number | compound(args) | [list,sugar]
  • Skip for phase 1: operator table. X is Y + 1 must be written is(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 in tests/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: each pl-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, ->/2 inside ;, call/1, write/1, nl/0 — all 11 done. write/1 and nl/0 use a global pl-output-buffer string + pl-output-clear! for testability; pl-format-term walks 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/2 with + - * / mod abspl-eval-arith walks deep, recurses on compounds, dispatches on functor; binary + - * / mod, binary AND unary -, unary abs. is/2 evaluates RHS, wraps as ("num" v), unifies via pl-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
    • nqueens.pl — 8-queens
    • family.pl — facts + rules (parent/ancestor)
  • lib/prolog/conformance.sh + runner, scoreboard.json + scoreboard.md
  • Target: all 5 classic programs passing

Phase 4 — operator table + more built-ins (next run)

  • Operator table parsing (prefix/infix/postfix, precedence, assoc)
  • assert/1, asserta/1, assertz/1, retract/1
  • findall/3, bagof/3, setof/3
  • copy_term/2, functor/3, arg/3, =../2
  • String/atom predicates

Phase 5 — Hyperscript integration

  • prolog-query primitive 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 — reverse.pl second 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 nested appends. Total 160 (+6).
  • 2026-04-25 — append.pl first classic program. lib/prolog/tests/programs/append.pl is the canonical 2-clause source; append.sx embeds 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. Helpers pl-ap-list-to-sx / pl-ap-term-to-sx convert deep-walked Prolog lists (("compound" "." (h t)) / ("atom" "[]")) to SX lists for structural assertion. Total 154 (+6).
  • 2026-04-25 — is/2 arithmetic landed. pl-eval-arith recursively evaluates ground RHS expressions (binary + - * /, mod; binary+unary -; unary abs); is/2 wraps the value as ("num" v) and unifies via pl-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/0 landed using global string buffer (pl-output-buffer + pl-output-clear! + pl-output-write!). pl-format-term walks deep + dispatches on atom/num/str/compound/var; pl-format-args recursively comma-joins. 7 new tests cover atom/num/compound formatting, conjunction order, var-walk, and nl. 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 — ->/2 if-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/1 landed. 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/1 opens a fresh inner cut-box (ISO opacity: cut inside call(G) commits G, not caller). 11 new tests in tests/solve.sx cover atoms+vars for \=, both branches + count for ;, and call/1 against atoms / compounds / bound goal vars. Total 121 (+11). Box not yet ticked — ->/2, write/1, nl/0 still pending.
  • 2026-04-25 — Cut (!/0) landed. pl-cut? predicate; solver functions all take a cut-box; pl-solve-user! creates a fresh inner-cut-box and snapshots outer-was-cut; pl-try-clauses! abandons alternatives when inner.cut OR (outer.cut transitioned false→true during this call). 6 new cut tests in tests/solve.sx covering 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. New tests/solve.sx 18/18 green covers atomic goals, =, conjunction, fact lookup, multi-solution count, recursive ancestor rule, trail-undo verification. Bug fix: pl-instantiate had 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-goal in runtime.sx. New tests/clausedb.sx 14/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)