# Datalog-on-SX: Datalog on the CEK/VM

Datalog is a declarative query language: a restricted subset of Prolog with no function
symbols, only relations. Programs are sets of facts and rules; queries ask what follows.
Evaluation is bottom-up (fixpoint iteration) rather than Prolog's top-down DFS — which
means no infinite loops, guaranteed termination, and efficient incremental updates.

The unique angle: Datalog is a natural companion to the Prolog implementation already in
progress (`lib/prolog/`). The parser and term representation can share infrastructure;
the evaluator is an entirely different fixpoint engine rather than a DFS solver.

End-state goal: **full core Datalog** (facts, rules, stratified negation, aggregation,
recursion) with a clean SX query API, and a demonstration of Datalog as a query engine
for rose-ash data (e.g. federation graph, content relationships).

## Ground rules

- **Scope:** only touch `lib/datalog/**` and `plans/datalog-on-sx.md`. Do **not** edit
  `spec/`, `hosts/`, `shared/`, `lib/prolog/**`, or other `lib/<lang>/`.
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
- **SX files:** use `sx-tree` MCP tools only.
- **Architecture:** Datalog source → term AST → fixpoint evaluator. No transpiler to SX AST —
  the evaluator is written in SX and works directly on term structures.
- **Reference:** Ramakrishnan & Ullman "A Survey of Deductive Database Systems";
  Dalmau "Datalog and Constraint Satisfaction".
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.

## Architecture sketch

```
Datalog source text
    │
    ▼
lib/datalog/tokenizer.sx   — atoms, variables, numbers, strings, punct (?- :- , . ( ) [ ])
    │
    ▼
lib/datalog/parser.sx      — facts: atom(args). rules: head :- body. queries: ?- goal.
    │                        No function symbols (only constants and variables in args).
    ▼
lib/datalog/db.sx          — extensional DB (EDB): ground facts; IDB: derived relations;
    │                        clause index by relation name/arity
    ▼
lib/datalog/eval.sx        — bottom-up fixpoint: semi-naive evaluation with delta sets;
    │                        stratification for negation; incremental update API
    ▼
lib/datalog/query.sx       — query API: (datalog-query db goal) → list of substitutions;
                             SX embedding: define facts/rules as SX data directly
```

Key differences from Prolog:
- **No function symbols** — args are atoms, numbers, strings, or variables only. No `f(a,b)`.
- **No cuts** — no procedural control.
- **Bottom-up** — derive all consequences of all rules before answering; no search tree.
- **Termination guaranteed** — no infinite derivation chains (no function symbols → finite Herbrand base).
- **Stratified negation** — `not(P)` legal iff P does not recursively depend on its own negation.
- **Aggregation** — `count`, `sum`, `min`, `max` over derived tuples (Datalog+).

## Roadmap

### Phase 1 — tokenizer + parser
- [x] Tokenizer: atoms (lowercase/quoted), variables (uppercase/`_`), numbers, strings,
      punct (`( )`, `,`, `.`), operators (`:-`, `?-`, `<=`, `>=`, `!=`, `<`, `>`, `=`,
      `+`, `-`, `*`, `/`), comments (`%`, `/* */`)
      Note: no function symbol syntax (no nested `f(...)` in arg position) — but the
      parser permits nested compounds for arithmetic; safety analysis (Phase 3) rejects
      non-arithmetic nesting.
- [x] Parser:
      - Facts: `parent(tom, bob).` → `{:head (parent tom bob) :body ()}`
      - Rules: `ancestor(X,Z) :- parent(X,Y), ancestor(Y,Z).`
              → `{:head (ancestor X Z) :body ((parent X Y) (ancestor Y Z))}`
      - Queries: `?- ancestor(tom, X).` → `{:query ((ancestor tom X))}`
        (`:query` value is always a list of literals; `?- p, q.` → `{:query ((p) (q))}`)
      - Negation: `not(parent(X,Y))` in body position → `{:neg (parent X Y)}`
- [x] Tests in `lib/datalog/tests/parse.sx` (18) and `lib/datalog/tests/tokenize.sx` (26).
      Conformance harness: `bash lib/datalog/conformance.sh` → 44 / 44 passing.

### Phase 2 — unification + substitution
- [x] Ported (not shared) from `lib/prolog/` — term walk, no occurs check.
- [x] `dl-unify t1 t2 subst` → extended subst dict, or `nil` on failure.
- [x] `dl-walk`, `dl-bind`, `dl-apply-subst`, `dl-ground?`, `dl-vars-of`.
- [x] Substitutions are immutable dicts keyed by variable name (string).
      Lists/tuples unify element-wise (used for arithmetic compounds too).
- [x] Tests in `lib/datalog/tests/unify.sx` (28). 72 / 72 conformance.

### Phase 3 — extensional DB + naive evaluation + safety analysis
- [x] EDB+IDB combined: `{:facts {<rel-name-string> -> (literal ...)}}` —
      relations indexed by name; tuples stored as full literals so they
      unify directly. Dedup on insert via `dl-tuple-equal?`.
- [x] `dl-add-fact! db lit` (rejects non-ground) and `dl-add-rule! db rule`
      (rejects unsafe). `dl-program source` parses + loads in one step.
- [x] Naive evaluation `dl-saturate! db`: iterate rules until no new tuples.
      `dl-find-bindings` recursively joins body literals; `dl-match-positive`
      unifies a literal against every tuple in the relation.
- [x] `dl-query db goal` → list of substitutions over `goal`'s vars,
      deduplicated. `dl-relation db name` for derived tuples.
- [x] Safety analysis at `dl-add-rule!` time: every head variable except
      `_` must appear in some positive body literal. Built-ins and negated
      literals do not satisfy safety. Helpers `dl-positive-body-vars`,
      `dl-rule-unsafe-head-vars` exposed for later phases.
- [x] Negation and arithmetic built-ins error cleanly at saturate time
      (Phase 4 / Phase 7 will swap in real semantics).
- [x] Tests in `lib/datalog/tests/eval.sx` (15): transitive closure,
      sibling, same-generation, grandparent, cyclic graph reach, six
      safety cases. 87 / 87 conformance.

### Phase 4 — built-in predicates + body arithmetic
Almost every real query needs `<`, `=`, simple arithmetic, and string
comparisons in body position. These are not EDB lookups — they're
constraints that filter bindings.
- [x] Recognise built-in predicates in body: `(< X Y)`, `(<= X Y)`, `(> X Y)`,
      `(>= X Y)`, `(= X Y)`, `(!= X Y)` and arithmetic forms `(is Z (+ X Y))`,
      `(is Z (- X Y))`, `(is Z (* X Y))`, `(is Z (/ X Y))`. Live in
      `lib/datalog/builtins.sx`.
- [x] `dl-eval-builtin` dispatches; `dl-eval-arith` recursively evaluates
      `(+ a b)` etc. with full nesting. `=` unifies; `!=` rejects equal
      ground terms.
- [x] Order-aware safety analysis (`dl-rule-check-safety`): walks body
      left-to-right tracking which vars are bound. `is`'s RHS vars must
      be already bound; LHS becomes bound. Comparisons require both
      sides bound. `=` is special-cased — at least one side bound binds
      the other. Negation vars must be bound (will be enforced fully in
      Phase 7).
- [x] Wired through SX numeric primitives — no separate number tower.
- [x] Tests in `lib/datalog/tests/builtins.sx` (19): range filters,
      arithmetic derivations, equality binding, eight safety violations
      and three safe-shape tests. Conformance 106 / 106.

### Phase 5 — semi-naive evaluation (performance)
- [x] Delta sets `{rel-name -> tuples}` track newly derived tuples per iter.
      `dl-snapshot-facts` builds the initial delta from the EDB.
- [x] Semi-naive rule: for each rule, walk every positive body literal
      position; substitute that one with the per-relation delta and join
      the rest against the previous-iteration DB (`dl-find-bindings-semi`).
      Candidates are collected before mutating the DB so the "full" sides
      see a consistent snapshot.
- [x] `dl-collect-rule-candidates` falls back to a naive single pass when
      a rule has no positive body literal (e.g. `(p X) :- (= X 5).`).
- [x] `dl-saturate!` is now semi-naive by default; `dl-saturate-naive!`
      kept for differential testing and a reference implementation.
- [x] Tests in `lib/datalog/tests/semi_naive.sx` (8) — every recursive
      program from earlier suites is run under both saturators with
      per-relation tuple counts compared (cheap, robust under bundled
      conformance session). A chain-5 differential exercises multiple
      semi-naive iterations against the recursive ancestor rule.
      Larger chains hit prohibitive wall-clock under conformance CPU
      contention with other agents — a future Blocker tracks switching
      `dl-tuple-member?` from O(n²) list scan to a hash-set per relation.

### Phase 6 — magic sets (goal-directed bottom-up, opt-in)
Naive bottom-up derives **all** consequences before answering. Magic sets
rewrite the program so the fixpoint only derives tuples relevant to the
goal — a major perf win for "what's reachable from node X" queries on
large graphs.
- [ ] Adornments: annotate rule predicates with bound (`b`) / free (`f`)
      patterns based on how they're called.
- [ ] Magic transformation: for each adorned predicate, generate a
      `magic_<pred>` relation and rewrite rule bodies to filter through it.
- [ ] Sideways information passing strategy (SIPS): left-to-right by
      default; pluggable.
- [ ] Optional pass — `(dl-set-strategy! db :magic)`; default semi-naive.
- [ ] Tests: equivalence vs naive on small inputs; perf win on a 10k-node
      reachability query from a single root.

### Phase 7 — stratified negation
- [x] Dependency graph: `dl-build-dep-graph db` returns `{head -> ({:rel
      :neg} ...)}`. Built-ins drop out (they're not relations).
- [x] Reachability via Floyd-Warshall in `dl-build-reach`; cycles
      detected by `reach[A][B] && reach[B][A]`. Programs are
      non-stratifiable iff any negative dependency falls inside an SCC.
      `dl-check-stratifiable` returns nil on success or a clear message.
- [x] `dl-compute-strata` propagates stratum numbers iteratively:
      `stratum(R) = max over deps of (stratum(dep) + (1 if negated else 0))`.
- [x] Saturator refactor: `dl-saturate-rules! db rules` is the semi-
      naive worker; `dl-saturate! db` rejects non-stratifiable programs,
      groups rules by head's stratum, and runs the worker on each
      stratum in increasing order.
- [x] `not(P)` in body: `dl-match-negation` walks the inner literal
      under the current subst and uses `dl-match-positive` — succeeds
      iff zero matches. Order-aware safety in `dl-rule-check-safety`
      (already present from Phase 4) requires negation vars to be
      bound by an earlier positive literal.
- [x] Tests in `lib/datalog/tests/negation.sx` (10): EDB and IDB
      negation, two-step strata, multi-level strata, with-arithmetic,
      empty-result and always-fail cases, non-stratifiability
      rejection, and a negation safety violation.

### Phase 8 — aggregation (Datalog+)
- [ ] `count(X, Goal)` → number of distinct X satisfying Goal
- [ ] `sum(X, Goal)` → sum of X values satisfying Goal
- [ ] `min(X, Goal)` / `max(X, Goal)` → min/max of X satisfying Goal
- [ ] `group-by` semantics: `count(X, sibling(bob, X))` → count of bob's siblings
- [ ] Aggregation breaks stratification — evaluate in a separate post-fixpoint pass
- [ ] Tests: social network statistics, grade aggregation, inventory sums

### Phase 9 — SX embedding API
- [ ] `(dl-program facts rules)` → database from SX data directly (no parsing required)
      ```
      (dl-program
        '((parent tom bob) (parent tom liz) (parent bob ann))
        '((ancestor X Z :- (parent X Y) (ancestor Y Z))
          (ancestor X Y :- (parent X Y))))
      ```
- [ ] `(dl-query db '(ancestor tom ?X))` → `((ann) (bob) (liz) (pat))`
- [ ] `(dl-assert! db '(parent ann pat))` → incremental fact addition + re-derive
- [ ] `(dl-retract! db '(parent tom bob))` → fact removal + re-derive from scratch
- [ ] Integration demo: federation graph query — `(ancestor actor1 actor2)` over
      rose-ash ActivityPub follow relationships

### Phase 10 — Datalog as a query language for rose-ash
- [ ] Schema: map SQLAlchemy model relationships to Datalog EDB facts
      (e.g. `(follows user1 user2)`, `(authored user post)`, `(tagged post tag)`)
- [ ] Loader: `dl-load-from-db!` — query PostgreSQL, populate Datalog EDB
- [ ] Query examples:
      - `?- ancestor(me, X), authored(X, Post), tagged(Post, cooking).`
        → posts about cooking by people I follow (transitively)
      - `?- popular(Post) :- tagged(Post, T), count(L, (liked(L, Post))) >= 10.`
        → posts with 10+ likes
- [ ] Expose as a rose-ash service endpoint: `POST /internal/datalog` with program + query

## Blockers

- **Hash-set membership for relations.** `dl-tuple-member?` uses a linear
  list scan; insert is O(n) and saturating chain-N pushes O(n²) → O(n³)
  total. Under bundled conformance (CPU shared with other loop agents)
  even chain-15 hits multi-minute wall-clock. Tests scoped to chain-5
  for now. Fix: maintain a `{tuple-key → true}` dict per relation
  alongside the list; key tuples by their serialized form.

## Progress log

_Newest first._

- 2026-05-08 — Phase 7 done (Phase 6 magic sets deferred — opt-in,
  semi-naive default suffices for current test suite). New
  `lib/datalog/strata.sx` (~290 LOC): dep graph build, Floyd-Warshall
  reachability, SCC-via-mutual-reachability for non-stratifiability
  detection, iterative stratum computation, rule grouping by head
  stratum. eval.sx split: `dl-saturate-rules!` is the per-rule-set
  semi-naive worker, `dl-saturate!` is now the stratified driver
  (errors out on non-stratifiable programs). `dl-match-negation` in
  eval.sx: succeeds iff inner positive match is empty. Stratum-keyed
  dicts use `(str s)` since SX dicts only accept string/keyword keys.
  10 negation tests cover EDB/IDB negation, multi-level strata,
  non-stratifiability rejection, and a negation safety violation.

- 2026-05-08 — Phase 5 done. `lib/datalog/eval.sx` rewritten to
  semi-naive default. `dl-saturate!` tracks a per-relation delta and
  on each iteration walks every positive body position substituting
  delta for that one literal — joining the rest against the full DB
  snapshot. `dl-saturate-naive!` retained as the reference. Rules
  with no positive body literal (e.g. `(p X) :- (= X 5).`) fall back
  to a naive one-shot via `dl-collect-rule-candidates`. 8 tests
  differentially compare the two saturators using per-relation tuple
  counts (cheap). Chain-5 differential exercises multi-iteration
  recursive saturation. Larger chains made conformance.sh time out
  due to O(n) `dl-tuple-member?` × CPU sharing with other loop
  agents — added a Blocker to swap to a hash-set for membership.
  Also tightened `dl-tuple-member?` to use indexed iteration instead
  of recursive `rest` (was creating a fresh list per step).

- 2026-05-07 — Phase 4 done. `lib/datalog/builtins.sx` (~280 LOC) adds
  `(< X Y)`, `(<= X Y)`, `(> X Y)`, `(>= X Y)`, `(= X Y)`, `(!= X Y)`,
  and `(is X expr)` with `+ - * /`. `dl-eval-builtin` dispatches;
  `dl-eval-arith` recursively evaluates nested compounds. Safety
  check is now order-aware — it walks body literals left-to-right
  tracking the bound set, requires comparison/`is` inputs to be
  already bound, and special-cases `=` (binds the var-side; both
  sides must include at least one bound to bind the other). Phase 3's
  simple safety check stays in db.sx as a forward-reference fallback;
  builtins.sx redefines `dl-rule-check-safety` to the comprehensive
  version. eval.sx's `dl-match-lit` now dispatches built-ins through
  `dl-eval-builtin`. 19 builtins tests; conformance 106 / 106.

- 2026-05-07 — Phase 3 done. `lib/datalog/db.sx` (~250 LOC) holds facts
  indexed by relation name plus the rules list, with `dl-add-fact!` /
  `dl-add-rule!` (rejects non-ground facts and unsafe rules);
  `lib/datalog/eval.sx` (~150 LOC) implements the naive bottom-up
  fixpoint via `dl-find-bindings`/`dl-match-positive`/`dl-saturate!`
  and `dl-query` (deduped projected substitutions). Safety analysis
  rejects unsafe head vars at load time. Negation and arithmetic
  built-ins raise clean errors (lifted in later phases). 15 eval
  tests cover transitive closure, sibling, same-generation, cyclic
  graph reach, and six safety violations. Conformance 87 / 87.

- 2026-05-07 — Phase 2 done. `lib/datalog/unify.sx` (~140 LOC):
  `dl-var?` (case + underscore), `dl-walk`, `dl-bind`, `dl-unify` (returns
  extended dict subst or `nil`), `dl-apply-subst`, `dl-ground?`, `dl-vars-of`.
  Substitutions are immutable dicts; `assoc` builds extended copies. 28
  unify tests; conformance now 72 / 72.

- 2026-05-07 — Phase 1 done. `lib/datalog/tokenizer.sx` (~190 LOC) emits
  `{:type :value :pos}` tokens; `lib/datalog/parser.sx` (~150 LOC) produces
  `{:head … :body …}` / `{:query …}` clauses, with nested compounds
  permitted for arithmetic and `not(...)` desugared to `{:neg …}`. 44 / 44
  via `bash lib/datalog/conformance.sh` (26 tokenize + 18 parse). Local
  helpers namespace-prefixed (`dl-emit!`, `dl-peek`) after a host-primitive
  shadow clash. Test harness uses a custom `dl-deep-equal?` that handles
  out-of-order dict keys and number repr (`equal?` fails on dict key order
  and on `30` vs `30.0`).
