;; 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))))))))