diff --git a/plans/minikanren-deferred.md b/plans/minikanren-deferred.md new file mode 100644 index 00000000..561572c2 --- /dev/null +++ b/plans/minikanren-deferred.md @@ -0,0 +1,216 @@ +# miniKanren-on-SX: deferred work + +The main plan (`plans/minikanren-on-sx.md`) carries Phases 1–7 through the +naive-tabling milestone. This file collects the four pieces left on the +shelf, with enough scope and design notes to drive a follow-up loop. + +Branch convention: keep the same `loops/minikanren` worktree; commit and +push to `origin/loops/minikanren`. Squash-merge to `architecture` only +when each numbered piece is shipped + tests green. + +Cumulative test count snapshot at squash-merge: **644** across +**71 test files**. Every change below should grow the number, not break +existing tests. + +## The four pieces + +### Piece A — Phase 7 SLG (cyclic patho, mutual recursion, fixed-point iteration) + +**Problem.** Naive tabling drains the answer stream eagerly, then caches. +Recursive tabled calls with the SAME ground key see an empty cache (the +in-progress entry never exists), so they recurse and the host +overflows. Fibonacci works only because each recursive call has a +*different* key; cyclic `patho` and any genuinely self-recursive tabled +predicate diverge. + +**Approach** — a small subset of SLG / OLDT resolution, enough to handle +the demos in the brief. + +1. **In-progress sentinel.** When a tabled call `T(args)` starts, store + `(:in-progress nil)` under its key. Recursive calls into `T(args)` + from inside its own computation see the sentinel and return only + the answers accumulated so far (initially empty). +2. **Answer accumulator.** As each new answer is found, push it into + the cache entry: `(:in-progress accumulated-answers)`. After a + cycling caller returns, the outer continuation can re-consult the + updated cache. +3. **Fixed-point iteration.** The driver repeatedly re-runs the goal + until no new answers appear in a full pass, then transitions the + cache from `:in-progress` to `:done`. +4. **Subgoal table.** Track (subgoal, last-seen-cache-version) per + subscriber so each consumer only re-reads what it hasn't seen. + +**Suggested artefacts.** +- `lib/minikanren/tabling-slg.sx` — new module with `table-slg-2` + (parallel to `table-2` from naive tabling). Keep `table-2` working + unchanged so Fibonacci/Ackermann don't regress. +- `lib/minikanren/tests/cyclic-graph-tabled.sx` — the canonical demo: + two-cycle `patho` from a→b→a→b plus a→b→c. With SLG, `(run* q + (tab-patho :a :c q))` returns the single shortest path, not divergence. +- `lib/minikanren/tests/mutual-recursion.sx` — even/odd via mutual + recursion (`even-o n` ↔ `odd-o (n-1)`), tabled at both names. + +**Reference reading.** +- TRS chapter on tabling. +- "Tabled Logic Programming" — Sagonas & Swift (the XSB / SLG paper). +- core.logic's `tabled` macro for an SX-dialect-friendly precedent. + +**Risk.** This is the brief's "research-grade complexity, not a +one-iteration item". Plan for 4–6 commits: in-progress sentinel, +answer accumulator, fixed-point driver, then one demo per commit. + +### Piece B — Phase 6 polish: bounds-consistency for `fd-plus` / `fd-times` + +**Problem.** Current `fd-plus-prop` and `fd-times-prop` propagate only +when two of three operands walk to ground numbers. When all three are +domain-bounded vars, the propagator returns `s` unchanged — search has +to label down to ground before any narrowing happens. + +**Approach** — narrow domains via interval reasoning even when no operand +is ground. + +For `(fd-plus x y z)` with bounded x, y, z: +- `x ∈ [z.min − y.max .. z.max − y.min]` +- `y ∈ [z.min − x.max .. z.max − x.min]` +- `z ∈ [x.min + y.min .. x.max + y.max]` + +For `(fd-times x y z)`: same shape, but with multiplication; need to +handle sign cases (negative domain ranges) and the divisor-when-not-zero +constraint already in place. + +**Suggested artefacts.** +- Patch `fd-plus-prop` and `fd-times-prop` in `lib/minikanren/clpfd.sx` + with new `:else` branches that compute new domain bounds and call + `fd-set-domain` for each var. +- New tests in `lib/minikanren/tests/clpfd-plus.sx` / + `clpfd-times.sx` exercising the all-domain case: two domain-bounded + vars in the body of a goal, with no labelling, after which their + domains have narrowed. +- A demo: cryptarithmetic puzzle (see Piece D) using bounds + consistency to avoid labelling explosion. + +**Risk.** Low. The math is well-known; just careful min/max arithmetic +and watch for edge cases (empty domain after narrowing). + +### Piece C — `=/=` disequality with constraint store + +**Problem.** `nafc` is sound only on ground args; `fd-neq` only on FD +domains. There is no general-purpose Prolog-style structural +disequality `=/=` that works on logic terms. + +**Approach.** Generalise the FD constraint store to a uniform +"constraint store" that carries: +- domain map (existing) +- *pending disequalities* — a list of `(u v)` pairs that must remain + non-unifiable under any future extension. + +After every `==` / `mk-unify`, re-check each pending disequality: +- If `(u v)` are now unifiable, fail. +- If they're now structurally distinct (no shared substitution can + unify), drop from the store (the constraint is satisfied). +- Otherwise leave in store. + +**Where it bites.** The kernel currently uses `mk-unify` everywhere. +Either: +1. Replace `mk-unify` with a constraint-aware wrapper everywhere + (intrusive, but principled). +2. Keep `mk-unify` for goals that don't use `=/=`, and provide a + parallel `==-cs` / `=/=-cs` pair plus an alternative `run*-cs` + driver that fires the constraint check after each binding. + +Option 2 mirrors the `fd-fire-store` pattern and stays out of the +common path. + +**Suggested artefacts.** +- `lib/minikanren/diseq.sx` — disequality store on top of the + existing `_fd` reserved key (re-using the constraint list, just + with disequality-shaped closures instead of FD propagators). +- `=/=` goal that posts a disequality and immediately checks it. +- `=/=-test` integration: rewrite a few Phase 5 puzzles using `=/=` + instead of `nafc + ==`. +- Tests covering: ground-pair fail, partial-pair satisfied later by + binding, partial-pair *contradicted* later by binding. + +**Risk.** Medium. The hard cases are *eventual* unifiability — a +disequality `(=/= (cons a 1) (cons 2 b))` should hold until both `a` +gets bound to `2` and `b` gets bound to `1`. Implementations like +core.logic's encode this as a list of "violating bindings" the +disequality remembers. + +### Piece D — Bigger CLP(FD) demos: send-more-money + Sudoku 4×4 + +**Problem.** The current N-queens demo only verifies the constraint +chain end-to-end. The brief's full Phase 6 list includes +"send-more-money, N-queens with CLP(FD), map coloring, +cryptarithmetic" — most of which exercise *more* than just `fd-neq + +fd-distinct`. + +**Approach.** Two concrete puzzles that both stress +bounds-consistency (Piece B) once it lands: + +#### send-more-money + +``` + S E N D ++ M O R E +--------- +M O N E Y +``` + +8 distinct digits ∈ {0..9}, S ≠ 0, M ≠ 0. Encoded as a sum-of-digits +equation using `fd-plus` + carry chains. + +Without Piece B (bounds-consistency), the search labels every digit +combination upfront — slow but tractable on a fast machine. With +Piece B, the impossible high-digit cases prune early. + +Test: a single solution `(9 5 6 7 1 0 8 2)`. + +#### Sudoku 4×4 + +Easier than 9×9 but exercises the full pattern: +- 16 cells, each ∈ {1..4} +- 4 rows, 4 cols, 4 2×2 boxes — 12 `fd-distinct` constraints +- Some cells fixed as clues + +A small solver should handle 4×4 in well under a second once +bounds-consistency narrows columns / boxes after each label step. + +**Suggested artefacts.** +- `lib/minikanren/tests/send-more-money.sx` — single-solution test. +- `lib/minikanren/tests/sudoku-4x4.sx` — at least three cluesets: + unique solution, multiple solutions, no solution. +- Optional: `lib/minikanren/sudoku.sx` with a parameterised + `sudoku-n` for both 4×4 and a 9×9 stress test. + +**Risk.** Low–medium for 4×4 + send-more-money once Piece B lands. +9×9 Sudoku is a stretch; treat it as a stretch goal once the smaller +demos are green. + +## Suggested ordering + +1. **Piece B first** (bounds-consistency for `fd-plus` / `fd-times`). + Self-contained, low-risk, and unlocks Piece D's harder puzzles. +2. **Piece D** (the two demos). Validates Piece B with concrete + puzzles. Doubles as the brief's missing canary tests. +3. **Piece C** (`=/=`). Independent track; once shipped, refactor the + pet/diff puzzles in Phase 5 to use it instead of nafc. +4. **Piece A** (SLG tabling). Last because it's the highest-risk + piece; do it when the rest of the library is stable so regressions + are easy to spot. + +## Operating ground rules (carry over from the original brief) + +- **Scope:** `lib/minikanren/**` and the two plan files (this one and + the original). +- **Commit cadence:** one feature per commit. Short factual messages + (`mk: piece B — bounds-consistency for fd-plus`). +- **Plan updates:** tick boxes here as pieces land; mirror status in + `plans/minikanren-on-sx.md` Roadmap. +- **Test discipline:** every commit ends with the cumulative count + green. No-regression rule from the original brief still applies. +- **`sx-tree` MCP only** for `.sx` edits. `sx_validate` after every + structural edit. +- **Pushing:** `origin/loops/minikanren` only. Never `main`. Squash to + `architecture` only with explicit user permission, as we did for + the v1 merge.