(defpackage :fwoar.lisp-sandbox.datalog (:use :cl ) (:export )) (in-package :fwoar.lisp-sandbox.datalog) (defun entity-gen (min-id max-id) (when (< max-id min-id) (rotatef max-id min-id)) (lambda () (+ min-id (random (- max-id min-id))))) (defun select (options) (let ((options (coerce options 'vector))) (lambda () (elt options (random (length options)))))) (defun fact-gen (entity-gen attribute-gen value-gen) (lambda () (list (funcall entity-gen) (funcall attribute-gen) (funcall value-gen)))) (defun facts-gen (n entity-gen attribute-gen value-gen) (let ((gen (fact-gen entity-gen attribute-gen value-gen))) (loop repeat n collect (funcall gen)))) (defun alist->triples (alist) (mapcar (data-lens:juxt 'sxhash 'car 'cdr) alist)) (defun plist->triples (plist) (loop for (k v) on plist by #'cddr collect (list (sxhash plist) k v))) (defgeneric to-triples (object) (:documentation "convert OBJECT to a list of EAV triples") (:method-combination append) (:method append ((object hash-table)) (serapeum:with-collector (c) (let ((object-id (sxhash object))) (maphash (lambda (k v) (typecase v (string (c (list object-id k v))) (vector (map nil (lambda (it) (c (list object-id k it))) v)) (t (c (list object-id k v))))) object)))) (:method append ((object package)) (serapeum:with-collector (c) (flet ((handle-symbol (s) (when (boundp s) (c (list object :binding/variable s)) (alexandria:when-let ((doc (documentation s 'variable))) (c (list s :documentation/variable doc)))) (cond ((macro-function s) (c (list object :binding/macro s))) ((fboundp s) (c (list object :binding/function s)))) (alexandria:when-let ((doc (documentation s 'function))) (c (list s :documentation/function doc))) (alexandria:when-let ((doc (documentation s 'setf))) (c (list s :documentation/setf doc))))) (c (list object :package/name (package-name object))) (mapcar (lambda (nickname) (c (list object :package/nickname nickname))) (package-nicknames object)) (mapcar (lambda (use) (c (list object :package/uses use))) (package-use-list object)) (do-symbols (s object) (when (eql object (symbol-package s)) (c (list object (case (nth-value 1 (find-symbol (symbol-name s) object)) (:external :symbol/external) (:internal :symbol/internal) (:inherited :symbol/accessible)) s)) (handle-symbol s)))))) (:method append ((object cons)) (serapeum:with-collector (c) (let ((object-id (sxhash object))) (destructuring-bind (car . cdr) object (if (consp car) (progn (c (list object-id :car (sxhash car))) (mapc #'c (to-triples car))) (c (list object-id :car car))) (if (consp cdr) (progn (c (list object-id :cdr (sxhash cdr))) (mapc #'c (to-triples cdr))) (c (list object-id :cdr cdr)))))))) (defmethod to-triples append ((object plump:element)) (serapeum:with-collector (c) (let ((object-id object)) (c (list object-id :tag (plump:tag-name object)) (list object-id :it object)) (alexandria:when-let* ((children (plump:children object)) (text (and (= 1 (length children)) (plump:textual-node-p (elt children 0)) (elt children 0)))) (c (list object-id :text (plump:text text)))) (map nil (lambda (it) (c (list object-id :child it))) (plump:child-elements object)) (c (list object-id :next (plump:next-element object))) (maphash (lambda (k v) (c (list object-id :attribute k)) (cond ((equal k "class") (mapcar (lambda (it) (c (list object-id k it))) (serapeum:split-sequence-if #'serapeum:whitespacep v))) (t (c (list object-id k v))))) (plump:attributes object)) (map nil (lambda (child) (mapc #'c (to-triples child))) (plump:child-elements object))))) (defvar *database* '() "The set of triples to query") (defvar *attribute-index*) (defvar *attribute-cardinality*) (defvar *entity-index*) (defun calculate-attribute-cardinality (database) (fw.lu:prog1-bind (result (make-hash-table :test 'equal)) (loop for triple in database for attribute = (attribute triple) do (incf (gethash attribute result 0))))) (defun ea-keygen () (lambda (triple) (list (list (entity triple) (attribute triple)) (list nil (attribute triple)) (list (entity triple) nil)))) (defun two-level-index (database key-gen) "Given a database and a key generator, create a hash-table with all the key-gen values" (loop with result = (make-hash-table :test 'equal) for triple in database do (loop for key in (funcall key-gen triple) do (push triple (gethash key result))) finally (return result))) (defun variablep (it) "Is IT a variable?" (and (symbolp it) (eql #\? (elt (symbol-name it) 0)))) (defgeneric entity (thing) (:documentation "get the entity for THING") (:method ((thing cons)) (first thing))) (defgeneric attribute (thing) (:documentation "get the attribute for THING") (:method ((thing cons)) (second thing))) (defgeneric value (thing) (:documentation "get the value for THING") (:method ((thing cons)) (third thing))) (defun filter-by-pattern (pattern bindings database) "Given BINDINGS, for each fact in DATABASE determine if PATTERN matches." (flet ((is-bound (variable) (cdr (assoc variable bindings)))) (let* ((database (let* ((entity (entity pattern)) (entity-b (is-bound entity)) (attribute (attribute pattern)) (attribute-b (is-bound attribute))) (cond ((and (not (variablep entity)) (not (variablep attribute))) (or (gethash (list entity attribute) *attribute-index*) database)) ((and (not (variablep entity))) (or (gethash (list entity nil) *attribute-index*) database)) ((and entity-b (not (variablep attribute))) (or (gethash (list entity-b attribute) *attribute-index*) database)) ((and entity-b attribute-b) (or (gethash (list entity-b attribute-b) *attribute-index*) database)) ((and (not (variablep attribute))) (or (gethash (list nil attribute) *attribute-index*) database)) ((and (not (variablep entity)) attribute-b) (or (gethash (list entity attribute-b) *attribute-index*) database)) (t database))))) (labels ((check-pattern-part (pattern target handle-binding) (cond ((consp pattern) (destructuring-bind (var . check) pattern (alexandria:if-let ((bound (serapeum:assocdr var bindings))) (when (equal bound (funcall check target)) t) (alexandria:when-let ((val (funcall check target))) (funcall handle-binding (cons var val)) t)))) ((variablep pattern) (alexandria:if-let ((bound (serapeum:assocdr pattern bindings))) (when (equal bound target) t) (progn (funcall handle-binding (cons pattern target)) t))) (t (equal target pattern)))) (check-pattern (triple) (let ((new-bindings ())) (values (every 'identity (mapcar (lambda (pat tar) (check-pattern-part pat tar (lambda (it) (push it new-bindings)))) pattern triple)) new-bindings)))) (let ((out-bindings ())) (dolist (triple database out-bindings) (multiple-value-bind (matched new-bindings) (check-pattern triple) (when matched (push new-bindings out-bindings))))))))) (defun index (selector database) (loop with result = (make-hash-table :test 'equal) for triple in database do (push triple (gethash (funcall selector triple) result)) finally (return result))) (defun match-patterns (patterns database) (let* ((*attribute-index* (two-level-index database (ea-keygen))) (out-matches (filter-by-pattern (car patterns) () database))) (dolist (pattern (cdr patterns) out-matches) (setf out-matches (mapcan (lambda (bindings) (remove-duplicates (mapcar (lambda (new-bindings) (append bindings new-bindings)) (filter-by-pattern pattern bindings database)) :test 'equal)) out-matches))))) (defun do-q (cb out-vars patterns database) (let ((results (match-patterns patterns database))) (remove nil (mapcar (data-lens:∘ (data-lens:applying cb) (apply #'data-lens:juxt (mapcar #'data-lens:key out-vars))) results)))) (defmacro q ((&rest out-vars) patterns &body body) `(do-q (lambda ,out-vars ,@body) ',out-vars ,patterns *database*)) (defun call-with-database (*database* cb) (funcall cb)) (defmacro with-database (database &body body) `(call-with-database ,database (lambda () ,@body)))