mimir/mimir0.1.0-SNAPSHOTMímir is an experimental rule engine written in Clojure dependencies
| (this space intentionally left almost blank) | ||||||||||||
(ns mimir.match (:use [clojure.set :only (intersection map-invert rename-keys difference union join)] [clojure.tools.logging :only (debug info warn error spy enabled?)] [clojure.walk :only (postwalk prewalk walk postwalk-replace)]) (:import [java.util.regex Pattern] [clojure.lang IPersistentMap IPersistentSet Sequential Symbol Fn Keyword])) | |||||||||||||
(defprotocol MatchAny (match-any [this x acc])) (defprotocol MatchMap (match-map [this x acc])) (defprotocol MatchSeq (match-seq [this x acc])) | |||||||||||||
(defn filter-walk [pred coll] (let [acc (transient [])] (postwalk #(when (pred %) (conj! acc %)) coll) (distinct (persistent! acc)))) | |||||||||||||
(defn singleton-coll? [x] (and (coll? (first x)) (not (next x)))) | |||||||||||||
(defn maybe-singleton-coll [x] (if (singleton-coll? x) (first x) x)) | |||||||||||||
(def default-match-var? #(and (symbol? %) (not (or (resolve %) ('#{do fn* let* if} %) (re-matches #".*/.*"(str %)) (re-matches #"\..*"(name %)) (re-matches #".*\."(name %)) (re-matches #".*#"(name %)))))) (def ^:dynamic *match-var?* default-match-var?) | |||||||||||||
(def ^:dynamic *var-symbol* symbol) | |||||||||||||
(defn bind-vars [x pattern acc] (if-let [var (if (*match-var?* pattern) pattern (-> pattern meta :tag))] (if (contains? acc var) (let [v (acc var)] (if-not (= v var) (if (= (acc v) var) (assoc acc var x) (match-any v x acc)) acc)) (assoc acc var x)) acc)) | |||||||||||||
(defn preserve-meta [form meta] (if (and (instance? clojure.lang.IMeta form) (not (and (list? form) (= 'quote (first form)) (symbol (second form))))) (list 'if (list 'instance? 'clojure.lang.IMeta form) (list 'with-meta form (list 'quote meta)) form) form)) | |||||||||||||
(defn meta-walk [form] (let [m (dissoc (meta form) :line)] (if (seq m) (preserve-meta (walk meta-walk identity form) m) (if (*match-var?* form) (list 'quote form) (walk meta-walk identity form))))) | |||||||||||||
(defn bound-vars [x] (let [vars (transient []) var-walk (fn this [form] (let [v (or (-> form meta :tag) form)] (when (*match-var?* v) (conj! vars v))) form)] (prewalk var-walk x) (distinct (persistent! vars)))) | |||||||||||||
(defn regex-vars [x] (let [vars (transient []) regex-walk (fn this [form] (when (instance? Pattern form) (reduce conj! vars (map (comp symbol second) (re-seq #"\(\?<(.+?)>.*?\)" (str form))))) form)] (postwalk regex-walk x) (distinct (persistent! vars)))) | |||||||||||||
(extend-type Object MatchAny (match-any [this x acc] (when (= this x) acc)) MatchMap (match-map [this x acc]) MatchSeq (match-seq [this x acc])) | |||||||||||||
(extend-type nil MatchAny (match-any [this x acc] (when (nil? x) acc)) MatchMap (match-map [this x acc]) MatchSeq (match-seq [this x acc] (when-not (seq x) acc))) | |||||||||||||
(extend-type IPersistentMap MatchAny (match-any [this x acc] (match-map x this acc)) MatchMap (match-map [x this acc] (loop [[k & ks] (keys this) acc acc] (if-not k (bind-vars x this acc) (when (contains? x k) (when-let [acc (match-any (this k) (x k) acc)] (recur ks (bind-vars (x k) (this k) acc)))))))) | |||||||||||||
(extend-type Symbol MatchAny (match-any [this x acc] (if (*match-var?* this) (bind-vars x this acc) (when (= this x) acc)))) | |||||||||||||
(extend-type Pattern MatchAny (match-any [this x acc] (let [re (re-matcher this (str x)) groups (regex-vars this)] (when (.matches re) (reduce #(assoc % (*var-symbol* %2) (.group re (str %2))) acc groups))))) | |||||||||||||
(extend-type Class MatchAny (match-any [this x acc] (when (instance? this x) acc))) | |||||||||||||
(extend-type Fn MatchAny (match-any [this x acc] (when (try (this x) (catch RuntimeException _)) (bind-vars x this acc)))) | |||||||||||||
(extend-type Keyword MatchAny (match-any [this x acc] (when (or (and (coll? x) (contains? x this)) (= x this)) (bind-vars x this acc))) MatchMap (match-map [this x acc] (when (contains? x this) (bind-vars x this acc)))) | |||||||||||||
(extend-type IPersistentSet MatchAny (match-any [this x acc] (loop [[k & ks] (seq this) acc acc] (when k (if-let [acc (match-any k x acc)] (bind-vars x this acc) (recur ks acc)))))) | |||||||||||||
(def rest? '#{& .}) | |||||||||||||
(extend-type Sequential MatchAny (match-any [this x acc] (match-seq x this acc)) MatchSeq (match-seq [x this acc] (loop [[p & ps] this [y & ys] x acc acc] (if (rest? y) (when (rest? p) (recur ps ys acc)) (if (and (not p) (not y)) (bind-vars x this acc) (if (rest? p) (let [rst (when y (vec (cons y ys)))] (when-let [acc (if (*match-var?* (first ps)) acc (match-seq rst (repeat (count rst) (first ps)) acc))] (bind-vars (or rst ()) (first ps) acc))) (when-let [acc (match-any p y acc)] (recur ps ys (bind-vars y p acc))))))))) | |||||||||||||
(defn truth [& _] true) | |||||||||||||
(defn unquote-vars-in-scope [&env form] (if &env (postwalk #(if (and (list? %) (= 'quote (first %)) (&env (second %))) (second %) %) form) form)) | |||||||||||||
(defn prepare-matcher [m &env] (->> (preserve-meta (walk identity meta-walk m) (meta m)) (postwalk-replace {'_ truth :else truth '. ''.}) (unquote-vars-in-scope &env))) | |||||||||||||
(defn match* [x pattern] (match-any pattern x {})) | |||||||||||||
(defmacro match [x m] `(match* ~x ~(prepare-matcher m &env))) | |||||||||||||
(defn all-vars [lhs] (vec (concat (bound-vars lhs) (map *var-symbol* (regex-vars lhs))))) | |||||||||||||
(defmacro condm* [match-var [lhs rhs & ms]] `(if-let [{:syms ~(remove (set (keys &env)) (all-vars lhs))} (mimir.match/match ~match-var ~lhs)] ~rhs ~(when ms `(condm* ~match-var ~ms)))) | |||||||||||||
(defmacro condm [x & ms] (let [match-var (if-let [v (-> x meta :tag)] v '*match*)] `(let [~match-var ~(if (and (instance? clojure.lang.IMeta x) (not (and (list? x) (= 'quote (first x)) (symbol? (second x))))) (with-meta x {}) x)] (condm* ~match-var ~ms)))) | |||||||||||||
(defn single-arg? [ms] (not-any? coll? (take-nth 2 ms))) | |||||||||||||
(defmacro fm [& ms] `(fn ~'this [& ~'args] (condm (if ~(single-arg? ms) (first ~'args) ~'args) ~@ms))) | |||||||||||||
(defmacro defm [name args & ms] (let [[doc ms] (split-with string? ms) [_ _ [match-var & _ ]] (partition-by '#{&} args)] `(do (defn ~name ~args ~(when (seq ms) `(condm ~(list 'first (if (single-arg? ms) (list 'first match-var) match-var)) ~@ms))) (alter-meta! (var ~name) merge {:doc (apply str ~doc)}) ~name))) | |||||||||||||
(ns mimir.mk (:use [clojure.tools.logging :only (debug info warn error spy)] [mimir.match :only (filter-walk prepare-matcher *match-var?* match-any bind-vars MatchAny MatchSeq)] [clojure.walk :only (postwalk-replace postwalk)]) (:import [java.io Writer] [clojure.lang Symbol Seqable]) (:refer-clojure :exclude [reify var? ==])) | |||||||||||||
mímirKanren: loosely based on "Implementation I: Core miniKanren", Chapter 3 in Byrd. | |||||||||||||
(defprotocol MatchVar (match-var [this x acc])) | |||||||||||||
(extend-protocol MatchVar Object (match-var [x this acc] (when-let [x (-> x meta :tag)] (match-any x this acc))) nil (match-var [x this acc]) Symbol (match-var [x this acc])) | |||||||||||||
(extend-protocol MatchAny Object (match-any [this x acc] (if (= this x) acc (match-var x this acc))) Symbol (match-any [this x acc] (if (= this x) acc (match-var x this acc))) nil (match-any [this x acc] (if (nil? x) acc (match-var x this acc)))) | |||||||||||||
(deftype LVar [name] MatchAny (match-any [this x acc] (if (= this x) acc (bind-vars x this acc))) MatchVar (match-var [x this acc] (match-any x this acc)) MatchSeq (match-seq [x this acc] (when ((every-pred sequential? seq) (acc x)) (match-any this (acc x) acc))) Object (hashCode [this] (if name (.hashCode name) 0)) (equals [this o] (and (instance? LVar o) (= (.name this) (.name ^LVar o))))) | |||||||||||||
(defmethod print-method LVar [o ^Writer w] (.write w (str (.name o)))) | |||||||||||||
(defmacro alias-macro [m a] `(doto (intern *ns* '~a (var ~m)) .setMacro)) | |||||||||||||
(defn var? [x] (instance? LVar x)) | |||||||||||||
(defn cons-pairs-to-seqs [x] (if (and (sequential? x) (= 3 (count x)) (= '. (second x)) ((some-fn sequential? nil?) (last x))) (cons (first x) (last x)) x)) | |||||||||||||
(defmacro unify [u v s] (let [[u v] (map #(prepare-matcher % &env) [u v])] `(binding [*match-var?* var?] (merge (match-any ~u ~v ~s) (match-any ~v ~u ~s))))) | |||||||||||||
(def ^:private subscripts '[₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉]) | |||||||||||||
(defn reify-name [n] (symbol (apply str "–" (map (comp subscripts int bigdec str) (str n))))) | |||||||||||||
(defn reify [v s] (loop [v v s s check #{v}] (let [v' (postwalk-replace s v)] (debug v') (if (contains? check v') v' (recur v' s (conj check v')))))) | |||||||||||||
(defmacro ≡ [u v] `(fn ≡ [a#] [(unify ~u ~v a#)])) (alias-macro ≡ ==) | |||||||||||||
(defmacro ≠ [u v] `(fn ≠ [a#] [(when-not (seq (select-keys (unify ~u ~v a#) (keys a#))) a#)])) (alias-macro ≠ !=) | |||||||||||||
(defn interleave-all [& colls] (when-let [ss (seq (remove nil? (map seq colls)))] (concat (map first ss) (lazy-seq (apply interleave-all (map rest ss)))))) | |||||||||||||
(defmacro condᵉ [& gs] (let [a (gensym "a")] `(fn condᵉ [~a] (interleave-all ~@(map #(do `(run-internal ~(vec %) [~a])) gs))))) (alias-macro condᵉ conde) | |||||||||||||
(defmacro fresh [[& x] & gs] `(let [~@(mapcat (fn [x] `[~x (LVar. (gensym '~x))]) x)] [~@gs])) | |||||||||||||
(defmacro project [[& x] & gs] (let [a (gensym "a")] `(fn project [~a] (let [~@(mapcat (fn [x] `[~x (~a ~x)]) x)] (run-internal ~(vec gs) [~a]))))) | |||||||||||||
(defn run-internal [gs s] (lazy-seq (let [[g & gs] (flatten gs) s (remove nil? s)] (if-not g s (mapcat #(when-let [s (g %)] (concat (run-internal gs [(first s)]) (run-internal gs (rest s)))) s))))) | |||||||||||||
(defn reify-goal [xs s] (let [xs (map #(reify % s) xs) vs (loop [[v & vs] (distinct (filter-walk var? xs)) acc {}] (if-not v acc (recur vs (assoc acc v (or (acc (s v)) (reify-name (count acc)))))))] (postwalk cons-pairs-to-seqs (postwalk-replace vs xs)))) | |||||||||||||
(defmacro run* [[& x] & g] (let [g (postwalk-replace {'_ '(mimir.mk.LVar. (gensym '_))} g)] `(binding [*match-var?* var?] (run-internal (fresh [~@x] ~@g (partial reify-goal ~(vec x))) [{}])))) | |||||||||||||
(defmacro run [n [& x] & g] `(take ~n (run* [~@x] ~@g))) | |||||||||||||
(def succeed (≡ false false)) (def fail (≡ false true)) | |||||||||||||
(defn consᵒ [a d l] (if (var? l) (let [d (if (var? d) ['. d] d)] (≡ (cons a d) l)) [(≡ a (first l)) (≡ d (rest l))])) | |||||||||||||
(defn firstᵒ [l a] (fresh [d] (consᵒ a d l))) | |||||||||||||
(defn restᵒ [l d] (fresh [a] (consᵒ a d l))) | |||||||||||||
(defn memberᵒ [x ls] (fresh [a d] (consᵒ a d ls) (condᵉ ((≡ a x)) ((memberᵒ x d))))) | |||||||||||||
(defn appendᵒ [l1 l2 o] (condᵉ ((≡ l1 ()) (≡ l2 o)) ((fresh [a d r] (consᵒ a d l1) (consᵒ a r o) (appendᵒ d l2 r))))) | |||||||||||||
(ns mimir.parse (:require [clojure.core.reducers :as r] [flatland.ordered.map :as om] [flatland.ordered.set :as os]) (:import [java.util.regex Pattern] [java.util Map Set List] [clojure.lang Keyword ArityException] [flatland.ordered.set OrderedSet])) | |||||||||||||
Mímir Parse | |||||||||||||
Experimental parser, this isn't built on some nice theoretical basis. Inspired by the awesome Instaparse: https://github.com/Engelberg/instaparse | |||||||||||||
Started out as an experiment in Ced: https://github.com/hraberg/ced/ This parser like the other parts of Mímir was written as a learning exercise. | |||||||||||||
See mimir.test.parse for examples (in various broken states). | |||||||||||||
(set! *warn-on-reflection* true) | |||||||||||||
(declare node maybe-singleton) | |||||||||||||
(def ^:dynamic *allow-split-tokens* true) ;; Overrides post-delimiter. | |||||||||||||
Overrides post-delimiter. | (def ^:dynamic *memoize* true) (def ^:dynamic *capture-string-literals* false) (def ^:dynamic *pre-delimiter* #"\s*") (def ^:dynamic *post-delimiter* #"(:?\s+|$)") (def ^:dynamic *offset* 0) (def ^:dynamic *rule* nil) (def ^:dynamic *default-result* []) (def ^:dynamic *token-fn* conj) (def ^:dynamic *suppress-tags* false) (def ^:dynamic *node-fn* #'node) (def ^:dynamic *default-action* #'maybe-singleton) (def ^:dynamic *grammar-actions* true) (def ^:dynamic *alternatives-rank* (comp count flatten :result)) (def ^:dynamic *grammar* {}) (def ^:dynamic *failure-grammar* {:no-match [#"\S*" #(throw (IllegalStateException. (str "Don't know how to parse: " %)))]}) (def ^:dynamic *start-rule* first) (def ^:dynamic *extract-result* (comp first :result)) (def ^:dynamic *rules-seen-at-point* #{}) | ||||||||||||
(defn maybe-singleton ([]) ([x] x) ([x & args] (vec (cons x args)))) | |||||||||||||
(defn suppressed-rule? [r] (when-let [[ _ r] (re-find #"^<(.+)>$" (name r))] (keyword r))) | |||||||||||||
(defn node? [x] (and (vector? x) (keyword? (first x)))) | |||||||||||||
(defn node [& args] (let [args (apply maybe-singleton args)] (if (or *suppress-tags* (suppressed-rule? *rule*)) args (if (and (sequential? args) (not (node? args))) (vec (cons *rule* args)) [*rule* args])))) | |||||||||||||
(defn suppressed-defintion? [r] (let [suppressed-defintion (keyword (str "<" (name r) ">"))] (if (*grammar* suppressed-defintion) suppressed-defintion r))) | |||||||||||||
(defrecord StringParser [string offset token result]) | |||||||||||||
(defn string-parser ([s] (if (instance? StringParser s) s (string-parser s *default-result*))) ([s result] (StringParser. s 0 nil result))) | |||||||||||||
(defn at-end? [{:keys [string offset] :as in}] (= offset (count string))) | |||||||||||||
(defn try-parse [{:keys [string offset result] :as in} ^Pattern re] (when in (let [m (re-matcher re (subs string offset))] (when (.lookingAt m) (assoc in :offset (+ offset (.end m 0)) :token (.group m 0)))))) | |||||||||||||
(defn try-parse-skip-delimiter [in m] (when-let [{:keys [token] :as in} (if-let [result (try-parse in m)] result (-> in (try-parse *pre-delimiter*) (try-parse m)))] (when-let [in (if *allow-split-tokens* in (try-parse in *post-delimiter*))] (assoc in :token token)))) | |||||||||||||
(defn next-token [in m capture?] (when-let [{:keys [token offset] :as in} (try-parse-skip-delimiter in m)] (assoc (if capture? (binding [*offset* offset] (-> in (update-in [:result] *token-fn* token))) in) :token nil))) | |||||||||||||
(defn name-and-predicate [n] (let [[_ predicate n] (re-find #"^([!&]?)(.+)" (name n))] [(keyword n) (when (seq predicate) (symbol predicate))])) | |||||||||||||
(defn name-and-quantifier [n] (let [[_ n quantifier] (re-find #"(.+?)([+*?]?)$" (name n))] [(keyword n) (when (seq quantifier) (symbol quantifier))])) | |||||||||||||
Not sure this name is right | (defprotocol IParser (parse [this] [this in])) | ||||||||||||
(defn fold-into [ctor coll] (r/fold (r/monoid into ctor) conj coll)) | |||||||||||||
This could potentially be a tree, but requires to restructure and use reducers all over the place. | (defn valid-choices [in ms] (fold-into vector (r/remove nil? (r/map #(parse % in) (vec ms))))) | ||||||||||||
(extend-protocol IParser Pattern (parse [this in] (next-token in this true)) Character (parse [this in] (parse (str this) in)) String (parse ([this] (parse (string-parser this))) ([this in] (next-token in (re-pattern (Pattern/quote this)) *capture-string-literals*))) Keyword (parse [this in] (when-not (*rules-seen-at-point* [this in]) ;; Only guards against StackOverflow, doesn't actually handle left recursion. (binding [*rules-seen-at-point* (conj *rules-seen-at-point* [this in])] (let [[this quantifier] (name-and-quantifier this) [this predicate] (name-and-predicate this) suppressed (suppressed-rule? this) this (suppressed-defintion? this)] (if-let [[rule action] (some *grammar* [this suppressed])] (letfn [(parse-one [in] (let [current-result (:result in)] (when-let [result (parse rule (assoc in :result *default-result*))] (binding [*rule* this] (update-in result [:result] #(*token-fn* current-result (*node-fn* (try (apply (or (when *grammar-actions* action) *default-action*) %) (catch ArityException _ (apply *default-action* %)))))))))) (parse-many [in quantifier] (case quantifier ? (or (parse-one in) in) * (loop [in in] (if-let [in (parse-one in)] (recur in) in)) + (when-let [in (parse-one in)] (parse-many in '*)) (parse-one in)))] (let [result (parse-many in quantifier)] (case predicate ! (when-not result in) & (when result in) result))) (throw (IllegalStateException. (str "Unknown rule: " this)))))))) Set (parse [this in] (when-let [alternatives (seq (distinct (valid-choices in this)))] (apply max-key :offset (sort-by *alternatives-rank* alternatives)))) OrderedSet (parse [this in] (first (valid-choices in this))) Map (parse [this in] (binding [*grammar* this] (parse (*start-rule* (os/into-ordered-set (keys this))) (string-parser in)))) List (parse [this in] (loop [in in [m & m-rst] this] (if (and in m (not (at-end? in))) (recur (parse m in) m-rst) (when-not m in)))) StringParser (parse ([this] (parse *grammar* this)) ([this parser] (parse parser this)))) | |||||||||||||
(def choice os/ordered-set) | |||||||||||||
(defn fun [s] (resolve (symbol s))) | |||||||||||||
(defn op ([op x] ((fun op) x)) ([x op y] ((fun op) x y))) | |||||||||||||
This feels a bit clunky | (defmacro dynamic-reader [] (let [locals (vec (keys &env))] `#(eval `(let [~'~locals ~~locals] ~(read-string %))))) | ||||||||||||
(def ^:dynamic *dynamic-reader*) | |||||||||||||
(defn action? [x] ((some-fn fn? var?) x)) | |||||||||||||
(defn rule? [r] (and (vector? r) (= 2 (count r)) (action? (last r)))) | |||||||||||||
(defn grammar [& rules] (let [rules (mapcat (fn [[rs [f]]] (if f (conj (vec (butlast rs)) [(last rs) f]) rs)) (partition-all 2 (partition-by action? rules)))] (into (om/ordered-map) (map (fn [[name rule]] [name (if (rule? rule) rule [rule])]) (partition 2 rules))))) | |||||||||||||
(defn parser-options [options] (into {} (map (fn [[k v]] [(if (keyword? k) (or (resolve (symbol (str "*" (name k) "*"))) (throw (IllegalArgumentException. (str "Unknown option: " k)))) k) v]) options))) | |||||||||||||
Starts getting clunky, holding off to macrofiy it as this is not the core issue. | (defn create-parser ([& rules] (let [[[default-options] rules] (split-with map? rules) default-options (parser-options default-options) grammar (apply grammar rules)] (fn parser ([in & options] (with-bindings (merge default-options (parser-options (apply hash-map options))) (let [real-parse parse] (try (when *memoize* ;; Just rebinding doesn't work for some reason (alter-var-root #'parse memoize)) (when-let [in (parse grammar in)] (if (at-end? in) (*extract-result* in) (parse *failure-grammar* in))) (finally (when *memoize* (alter-var-root #'parse (constantly real-parse)))))))))))) | ||||||||||||
(ns mimir.well (:use [clojure.set :only (intersection map-invert rename-keys difference union join)] [clojure.tools.logging :only (debug info warn error spy)] [clojure.walk :only (postwalk postwalk-replace)] [mimir.match :only (filter-walk maybe-singleton-coll match all-vars *match-var?* default-match-var?)]) (:require [clojure.core.reducers :as r]) (:refer-clojure :exclude [assert]) (:gen-class)) | |||||||||||||
(defn create-net [] {:productions #{} :working-memory #{} :predicates {} :predicate-invokers {} :expression-cache {} :alpha-network {} :beta-join-nodes {}}) | |||||||||||||
(def ^:dynamic *net* (atom (create-net))) | |||||||||||||
(defn dbg [x] (println x) x) | |||||||||||||
(doseq [k (keys @*net*)] (eval `(defn ~(symbol (name k)) [] (~k @*net*)))) | |||||||||||||
(defn triplet? [x] (and (sequential? x) (= 3 (count x)) (symbol? (second x)))) | |||||||||||||
(defn is-var? [x] (when-let [^String s (and (symbol? x) (name x))] (or (.startsWith s "?") (re-matches #"[A-Z]+" s)))) | |||||||||||||
(defn var-sym [x] (symbol (str "?" x))) | |||||||||||||
(alter-var-root #'*match-var?* (constantly (every-pred default-match-var? (complement is-var?)))) | |||||||||||||
(defn is-matcher? [x xs] (and (is-var? x) (not (symbol? (first xs))))) | |||||||||||||
(defn matcher? [c] (and (sequential? c) (= 'mimir.match/match* (first c)))) | |||||||||||||
(defn parser ([x] (parser x identity identity)) ([x atom-fn triplet-fn] (parser x atom-fn triplet-fn true)) ([[x & xs] atom-fn triplet-fn match] (when x (cond (and match ((some-fn map? set? vector?) x)) (cons (atom-fn (list 'mimir.match/match (gensym "?") x)) (parser xs atom-fn triplet-fn match)) ((some-fn sequential? map? set? string?) x) (cons (atom-fn x) (parser xs atom-fn triplet-fn match)) (and match (is-matcher? x xs)) (cons (atom-fn (list 'mimir.match/match x (first xs))) (parser (rest xs) atom-fn triplet-fn match)) (triplet? (cons x (take 2 xs))) (cons (triplet-fn (cons x (take 2 xs))) (parser (drop 2 xs) atom-fn triplet-fn match)) :else (cons x (parser xs atom-fn triplet-fn match)))))) | |||||||||||||
(defn quote-non-vars [rhs] (postwalk #(if (and (symbol? %) (not (is-var? %))) (list 'quote %) %) rhs)) | |||||||||||||
(defn vars [x] (filter-walk is-var? x)) | |||||||||||||
(defn quote-fact [t] (list 'quote t)) | |||||||||||||
(defn expand-rhs [t] (cons 'mimir.well/assert t)) | |||||||||||||
(def relations (reduce (fn [m rel] (assoc m rel rel)) '{<- mimir.well/bind = mimir.match/match* != not=} '[< > <= => not=])) | |||||||||||||
(defn macroexpand-conditions [lhs] (loop [[c & cs] (map macroexpand lhs) acc []] (if-not c acc (recur cs (if (every? seq? c) (into acc c) (conj acc c)))))) | |||||||||||||
(defn expand-lhs [t] (if-let [rel (relations (second t))] (let [[var _ & [rest]] t] (if-let [rest (and (seq? rest) (macroexpand-conditions [rest]))] (concat (butlast rest) [(list rel var (last rest))]) (list rel var rest))) t)) | |||||||||||||
(defn ellipsis ([x] (ellipsis 5 x)) ([n x] (let [[start more] (split-at n (take (inc n) x))] (str (seq start) (when more (str "... [total: " (count x) "]")))))) | |||||||||||||
(defn binding? [c] (and (sequential? c) (= 'mimir.well/bind (first c)))) | |||||||||||||
(defn binding-var [c] (when (binding? c) (second c))) | |||||||||||||
(defn binding-vars-for-rule [cs] (set (map binding-var (filter binding? cs)))) | |||||||||||||
(defn purge-match-vars [xs] (let [match-vars (remove is-var? (keys xs))] (apply dissoc xs (concat (map var-sym match-vars) match-vars)))) | |||||||||||||
(defmacro rule [name & body] (let [body (if ('#{=>} (first body)) (cons (list (gensym "?") '<- true) body) body) [body salience] (if (#{:salience} (first body)) [(drop 2 body) (second body)] [body 0]) [lhs _ rhs] (partition-by '#{=>} body) [doc lhs] (split-with string? lhs) expanded-lhs (->> (macroexpand-conditions (parser lhs expand-lhs expand-lhs)) (map #(with-meta % {:ns *ns*}))) rhs (parser rhs identity expand-rhs false) binding-vars (binding-vars-for-rule expanded-lhs)] `(let [f# (defn ~name ([] (~name {})) ([{:syms ~(vec (vars lhs)) :as ~'args}] (~name (working-memory) ~'args)) ([~'wm ~'args] (debug "rule" '~name '~*ns*) (for [vars# (check-rule '~(vec expanded-lhs) ~'wm ~'args) :let [{:syms ~(vec (concat (all-vars lhs) (vars lhs)))} vars# ~'*matches* (map val (sort-by key (dissoc (purge-match-vars vars#) '~@binding-vars)))]] (do (debug "rhs" vars#) ~@rhs))))] (debug "defining rule" '~name) (when-not (= '~lhs '~expanded-lhs) (debug "expanded" '~lhs) (debug " into" '~expanded-lhs)) (alter-meta! f# merge {:lhs '~lhs :rhs '~rhs :doc ~(apply str doc) :salience ~salience}) (swap! *net* update-in [:productions] conj f#) f#))) | |||||||||||||
(defmacro with-cache [cache-name key & f] (let [cache-name (keyword cache-name)] `(let [key# ~key] (if-not (contains? ('~cache-name @*net*) key#) (let [v# (do ~@f)] (swap! *net* assoc-in ['~cache-name key#] v#) v#) (get-in @*net* ['~cache-name key#]))))) | |||||||||||||
(defn join-on [x y] (let [vars-and-match-vars #(set (concat (remove '#{_} (all-vars %)) (vars %)))] (intersection (vars-and-match-vars x) (vars-and-match-vars y)))) | |||||||||||||
(defn var-to-index [c] (loop [[v & vs] (vars c) acc {}] (if v (recur vs (if (acc v) acc (assoc acc v (var-sym (inc (count acc)))))) acc))) | |||||||||||||
(defn ordered-vars [c] (->> (var-to-index c) vals sort vec)) | |||||||||||||
(defn tree-eval-walk [locals] (fn [form] (condp some [form] seq? (with-cache expression-cache form (eval form)) locals (locals form) form))) | |||||||||||||
(defmacro tree-eval [tree] (let [locals (keys (select-keys &env (filter-walk symbol? tree))) locals (into {} (map #(vector (list 'quote %) %) locals))] `(let [real-locals# ~locals] (postwalk (tree-eval-walk real-locals#) '~tree)))) | |||||||||||||
(defn uses-*matches*? [c] (boolean (some '#{*matches*} (flatten c)))) | |||||||||||||
(defn predicate-for [c] (with-cache predicate c (let [args (ordered-vars c) src `(fn [~@args & [~'*matches*]] ~c) meta (meta c)] (debug " compiling" c) (binding [*ns* (or (:ns meta) *ns*)] (with-meta (eval src) (merge meta {:src c :args args :uses-*matches* (uses-*matches*? c)})))))) | |||||||||||||
(defn alias-match-vars [m] (merge m (zipmap (map (comp var-sym name) (keys m)) (vals m)))) | |||||||||||||
(defn match-using-predicate [c wme] (let [predicate (predicate-for c)] (try (when-let [result (predicate wme)] (debug " evaluated to true" wme) (merge {'?1 wme} (when (matcher? c) (alias-match-vars result)))) (catch RuntimeException e (debug " threw non fatal" e))))) | |||||||||||||
(defn match-triplet [c wme] (loop [[v & vs] wme [t & ts] c m {}] (if v (condp some [t] #{v} (recur vs ts m) is-var? (recur vs ts (assoc m t v)) nil) (do (debug " evaluated to true" wme) m)))) | |||||||||||||
(defn predicate? [c] (-> c first ((some-fn (every-pred symbol? (partial ns-resolve (or (-> c meta :ns) *ns*))) (every-pred (complement symbol?) ifn?))))) | |||||||||||||
(defn bind [to expr] expr) (defn constraint [expr] expr) | |||||||||||||
(defn constraint? [c] (and (sequential? c) (= 'mimir.well/constraint (first c)))) | |||||||||||||
(defn multi-var-predicate? [c] (and (predicate? c) (or (> (count (vars c)) 1) (constraint? c)))) | |||||||||||||
(defn multi-var-predicate-placeholder [c] (let [pred (predicate-for c)] (debug " more than one argument, needs beta network") (with-meta (zipmap (-> pred meta :args) (repeat pred)) (assoc (meta pred) :pred pred)))) | |||||||||||||
(defn match-wme [c wme] (if (predicate? c) (match-using-predicate c wme) (match-triplet c wme))) | |||||||||||||
(defn ^:private wm-crud [action test msg fact] (when (test (working-memory) fact) (debug msg " fact" fact) (swap! *net* update-in [:working-memory] action fact) (doseq [c (keys (:alpha-network @*net*)) :let [match (match-wme c fact)] :when match] (debug " alpha network change" match) (swap! *net* update-in [:alpha-network] #(merge-with action % {c match})))) fact) | |||||||||||||
(defn fact [fact] (wm-crud conj (complement contains?) "asserting" fact)) | |||||||||||||
(defn retract* [fact] (wm-crud disj contains? "retracting" fact)) | |||||||||||||
(defn update [fact f & args] (let [wm (or (first (filter #(match % fact) (working-memory))) fact)] (retract* wm) (mimir.well/fact (condp some [f] fn? (apply f wm args) vector? (let [[a & _] args args (if (fn? a) args [(constantly a)])] (apply update-in wm f args)) f)))) | |||||||||||||
(defmacro facts [& wms] (when wms `(doall (for [wm# ~(vec (parser wms identity quote-fact false))] (fact wm#))))) | |||||||||||||
(defn fold-into [ctor coll] (r/fold (r/monoid into ctor) conj coll)) | |||||||||||||
(defn matching-wmes ([c] (matching-wmes c (working-memory) false)) ([c wm needs-beta?] (debug "condition" c) (if (or ((some-fn multi-var-predicate? binding?) c) needs-beta?) #{(multi-var-predicate-placeholder c)} (->> wm (map #(match-wme c %)) (remove nil?) set)))) | |||||||||||||
(defn alpha-network-lookup [c wm needs-beta?] (with-cache alpha-network c (matching-wmes c wm needs-beta?))) | |||||||||||||
(defn alpha-memory ([c] (alpha-memory c (working-memory) false)) ([c wm needs-beta?] (let [var-to-index (var-to-index c) vars-by-index (map-invert var-to-index)] (->> (alpha-network-lookup (with-meta (postwalk-replace var-to-index c) (meta c)) wm needs-beta?) (map #(rename-keys (with-meta % (merge (meta %) (postwalk-replace vars-by-index (meta %)))) vars-by-index)))))) | |||||||||||||
(defn cross [left right] (debug " nothing to join on, treating as or") (set (for [x left y right] (merge x y)))) | |||||||||||||
(defn multi-var-predicate-node? [am] (and (seq? am) (= 1 (count am)) (fn? (-> am first meta :pred)))) | |||||||||||||
(defn permutations* [n coll] (if (zero? n) [[]] (->> (permutations* (dec n) coll) (r/mapcat #(r/map (fn [x] (cons x %)) coll))))) | |||||||||||||
(defn permutations ([coll] (permutations (count coll) coll)) ([n coll] (fold-into vector (permutations* n coll)))) | |||||||||||||
(defn predicate-invoker [args join-on binding-vars uses-*matches*] (with-cache predicate-invokers [args join-on binding-vars uses-*matches*] (eval `(fn [pred# {:syms [~@(filter join-on args)] :as matches#}] (let [matches# (when ~uses-*matches* (vals (dissoc (purge-match-vars matches#) '~@binding-vars)))] (fn [[~@(remove join-on args)]] (pred# ~@args matches#))))))) | |||||||||||||
(defn deal-with-multi-var-predicates [c1-am c2-am join-on c2 binding-vars] (let [pred (-> c2-am first meta :pred) args (-> c2-am first meta :args) bind-var (binding-var c2) matcher ((some-fn matcher? constraint? c2)) uses-*matches* (-> pred meta :uses-*matches*) join-on (if bind-var (conj join-on bind-var) join-on) needed-args (vec (remove join-on args)) permutated-wm (permutations (count needed-args) (working-memory)) invoker (predicate-invoker args join-on binding-vars uses-*matches*) join-fn (fn [m] (let [invoker (invoker pred m)] (->> permutated-wm (r/map (fn [wm] (try (when-let [r (invoker wm)] (merge m (zipmap needed-args wm) (when matcher (alias-match-vars r)) (when bind-var {bind-var r}))) (catch RuntimeException e (debug " threw non fatal" e))))) (r/remove nil?))))] (debug " multi-var-predicate") (debug " args" args) (debug " known args" join-on "- need to find" needed-args) (debug " permutations of wm" (ellipsis permutated-wm)) (->> c1-am (r/mapcat join-fn) (fold-into vector)))) | |||||||||||||
(defn beta-join-node [c2 c1-am binding-vars wm] (let [c2-am (alpha-memory c2 wm (some binding-vars (vars c2)))] (with-cache beta-join-nodes [c1-am c2-am] (let [join-on (join-on (-> c1-am first keys) c2)] (debug "join" join-on) (debug " left" (ellipsis c1-am)) (debug " right" (ellipsis c2-am)) (let [result (cond (multi-var-predicate-node? c2-am) (deal-with-multi-var-predicates c1-am c2-am join-on c2 binding-vars) (empty? join-on) (cross c1-am c2-am) :else (join c1-am c2-am))] (debug "result" (ellipsis result)) result))))) | |||||||||||||
(defn order-conditions [cs] (mapcat #(concat (sort-by (comp count vars) (remove constraint? %)) (filter constraint? %)) (partition-by binding? cs))) | |||||||||||||
(defn check-rule [cs wm args] (debug "conditions" cs) (let [binding-vars (binding-vars-for-rule cs)] (loop [[c & cs] (order-conditions cs) matches #{args}] (if-not c matches (recur cs (beta-join-node c matches binding-vars wm)))))) | |||||||||||||
(defn salience [p] (or (-> p meta :salience) 0)) | |||||||||||||
(defn run-once ([] (run-once (working-memory) (productions))) ([wm productions] (->> productions (sort-by salience) vec ;; This is not thread safe. ;; (r/mapcat #(% wm {})) ;; (fold-into vector) (mapcat #(% wm {})) doall))) | |||||||||||||
(defn run* ([] (repeatedly run-once))) | |||||||||||||
(defn run ([] (run *net*)) ([net] (binding [*net* net] (loop [wm (working-memory) productions (:productions @net) acc #{}] (let [acc (union (set (run-once wm productions)) acc)] (if (seq (difference (working-memory) wm)) (recur (working-memory) productions acc) acc)))))) | |||||||||||||
(defn reset [] (reset! *net* (create-net))) | |||||||||||||
rule writing fns | |||||||||||||
(defmacro assert ([fact] `(let [fact# (list ~@(quote-non-vars fact))] (fact fact#))) ([id rel attr] `(assert ~(list id rel attr)))) | |||||||||||||
(defmacro retract ([fact] `(let [fact# (list ~@(quote-non-vars fact))] (retract* fact#))) ([id rel attr] `(retract ~(list id rel attr)))) | |||||||||||||
(defn different* [f xs] (apply distinct? (map f (maybe-singleton-coll xs)))) | |||||||||||||
(defmacro different ([f] `(different ~f ~'*matches*)) ([f xs] (if ((some-fn set? vector?) f) (map #(do `(constraint (different ~% ~xs))) f) `(constraint (different* ~f ~xs))))) | |||||||||||||
(defmacro all-different ([] `(different identity)) ([& xs] `(different identity ~(vec xs)))) | |||||||||||||
(defn same* ([test pred xs] (test (for [x xs y (remove #{x} xs)] (pred x y))))) | |||||||||||||
(defmacro not-same ([pred] `(not-same ~pred ~'*matches*)) ([pred xs] (if ((some-fn set? vector?) pred) (map #(do `(constraint (not-same ~% ~xs))) pred) `(constraint (same* (partial not-any? true?) ~pred (maybe-singleton-coll ~xs)))))) | |||||||||||||
(defn same [pred & xs] (if ((some-fn set? vector?) pred) (map #(list 'same % xs) pred) `(same* (partial every? true?) ~pred (maybe-singleton-coll ~xs)))) | |||||||||||||
(defmacro gen-vars ([n] `(gen-vars ~n ~(gensym))) ([n prefix] `(vec (map #(var-sym (str '~prefix "-" %)) (range 1 (inc ~n)))))) | |||||||||||||
(defmacro unique [xs] (concat (for [[x y] (partition 2 1 xs)] `(pos? (compare ~x ~y))) (list (list 'identity xs)))) | |||||||||||||
(defmacro take-unique [n] `(unique ~(gen-vars (eval n)))) | |||||||||||||
(defmacro take-distinct [n] `(identity ~(gen-vars (eval n)))) | |||||||||||||
(defn not-in [set] (complement set)) | |||||||||||||
(defn is-not [x] (partial not= x)) | |||||||||||||
(defmacro constrained-match [m x] `(some #(match % ~m) ~x)) | |||||||||||||
(defmacro constrain ([m] `(constraint (constrained-match ~m ~'*matches*))) ([x m]`(constraint (constrained-match ~m ~x)))) | |||||||||||||
(defn version [] (-> "project.clj" clojure.java.io/resource slurp read-string (nth 2))) | |||||||||||||
(defn -main [& args] (println) (println "Welcome to Mímir |" (version) "| Copyright © 2012-13 Håkan Råberg") (println) (require 'clojure.main) (clojure.main/repl :init #(in-ns 'mimir.well))) | |||||||||||||