Files
rose-ash/lib/minikanren/tabling.sx
giles 57a84b372d
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 57s
Merge loops/minikanren into architecture: full miniKanren-on-SX library
Squash merge of 76 commits from loops/minikanren. Adds lib/minikanren/
— a complete miniKanren-on-SX implementation built on top of
lib/guest/match.sx, validating the lib-guest unify-and-match kit as
intended.

Modules (20 .sx files, ~1700 LOC):
  unify, stream, goals, fresh, conde, condu, conda, run, relations,
  peano, intarith, project, nafc, matche, fd, queens, defrel, clpfd,
  tabling

Phases 1–5 fully done (core miniKanren API, all classic relations,
matche, conda, project, nafc).

Phase 6 — native CLP(FD): domain primitives, fd-in / fd-eq / fd-neq /
fd-lt / fd-lte / fd-plus / fd-times / fd-distinct / fd-label, with
constraint reactivation iterating to fixed point. N-queens via FD:
4-queens 2 solutions, 5-queens 10 solutions (vs naive timeout past N=4).

Phase 7 — naive ground-arg tabling: table-1 / table-2 / table-3.
Fibonacci canary: tab-fib(25) = 75025 in seconds, naive fib(25) times
out at 60s. Ackermann via table-3: A(3,3) = 61.

71 test files, 644+ tests passing across the suite. Producer/consumer
SLG (cyclic patho, mutual recursion) deferred — research-grade work.

The lib-guest validation experiment is conclusive: lib/minikanren/
unify.sx adds ~50 lines of local logic (custom cfg, deep walk*, fresh
counter) over lib/guest/match.sx's ~100-line kit. The kit earns its
keep ~3× by line count.
2026-05-08 23:01:54 +00:00

158 lines
4.9 KiB
Plaintext

;; lib/minikanren/tabling.sx — Phase 7 piece A: naive memoization.
;;
;; A `table-2` wrapper for 2-arg relations (input, output). Caches by
;; ground input (walked at call time). On hit, replays the cached output
;; values; on miss, runs the relation, collects all output values from
;; the answer stream, stores, then replays.
;;
;; Limitations of naive memoization (vs proper SLG / producer-consumer
;; tabling):
;; - Each call must terminate before its result enters the cache —
;; so cyclic recursive calls with the SAME ground input would still
;; diverge (not addressed here).
;; - Caching by full ground walk only; partially-ground args fall
;; through to the underlying relation.
;;
;; Despite the limitations, naive memoization is enough for the
;; canonical demo: Fibonacci goes from exponential to linear because
;; each fib(k) result is computed at most once.
;;
;; Cache lifetime: a single global mk-tab-cache. Use `(mk-tab-clear!)`
;; between independent queries.
(define mk-tab-cache {})
(define mk-tab-clear! (fn () (set! mk-tab-cache {})))
(define
mk-tab-lookup
(fn
(key)
(cond
((has-key? mk-tab-cache key) (get mk-tab-cache key))
(:else :miss))))
(define
mk-tab-store!
(fn (key vals) (set! mk-tab-cache (assoc mk-tab-cache key vals))))
(define
mk-tab-ground-term?
(fn
(t)
(cond
((is-var? t) false)
((mk-cons-cell? t)
(and
(mk-tab-ground-term? (mk-cons-head t))
(mk-tab-ground-term? (mk-cons-tail t))))
((mk-list-pair? t) (every? mk-tab-ground-term? t))
(:else true))))
(define
mk-tab-replay-vals
(fn
(vals output s)
(cond
((empty? vals) mzero)
(:else
(let
((sp (mk-unify output (first vals) s)))
(let
((this-stream (cond ((= sp nil) mzero) (:else (unit sp)))))
(mk-mplus this-stream (mk-tab-replay-vals (rest vals) output s))))))))
(define
table-2
(fn
(name rel-fn)
(fn
(input output)
(fn
(s)
(let
((winput (mk-walk* input s)))
(cond
((mk-tab-ground-term? winput)
(let
((key (str name "@" winput)))
(let
((cached (mk-tab-lookup key)))
(cond
((= cached :miss)
(let
((all-substs (stream-take -1 ((rel-fn input output) s))))
(let
((vals (map (fn (s2) (mk-walk* output s2)) all-substs)))
(begin
(mk-tab-store! key vals)
(mk-tab-replay-vals vals output s)))))
(:else (mk-tab-replay-vals cached output s))))))
(:else ((rel-fn input output) s))))))))
;; --- table-1: 1-arg relation (one input, no output to cache) ---
;; The relation is a predicate `(p input)` that succeeds or fails.
;; Cache stores either :ok or :no.
(define
table-1
(fn
(name rel-fn)
(fn
(input)
(fn
(s)
(let
((winput (mk-walk* input s)))
(cond
((mk-tab-ground-term? winput)
(let
((key (str name "@1@" winput)))
(let
((cached (mk-tab-lookup key)))
(cond
((= cached :miss)
(let
((stream ((rel-fn input) s)))
(let
((peek (stream-take 1 stream)))
(cond
((empty? peek)
(begin (mk-tab-store! key :no) mzero))
(:else (begin (mk-tab-store! key :ok) stream))))))
((= cached :ok) (unit s))
((= cached :no) mzero)
(:else mzero)))))
(:else ((rel-fn input) s))))))))
;; --- table-3: 3-arg relation (input1 input2 output) ---
;; Cache keyed by (input1, input2). Output values cached as a list.
(define
table-3
(fn
(name rel-fn)
(fn
(i1 i2 output)
(fn
(s)
(let
((wi1 (mk-walk* i1 s)) (wi2 (mk-walk* i2 s)))
(cond
((and (mk-tab-ground-term? wi1) (mk-tab-ground-term? wi2))
(let
((key (str name "@3@" wi1 "/" wi2)))
(let
((cached (mk-tab-lookup key)))
(cond
((= cached :miss)
(let
((all-substs (stream-take -1 ((rel-fn i1 i2 output) s))))
(let
((vals (map (fn (s2) (mk-walk* output s2)) all-substs)))
(begin
(mk-tab-store! key vals)
(mk-tab-replay-vals vals output s)))))
(:else (mk-tab-replay-vals cached output s))))))
(:else ((rel-fn i1 i2 output) s))))))))