src/wam/compile.lisp @ a9bdea1a9564

Clean up topological-sort

We don't actually need to get the full set of minimal elements on each iteration
because we don't need to break ties.  It'll be faster (and cleaner) to just grab
the first one we find.
author Steve Losh <steve@stevelosh.com>
date Sat, 26 Mar 2016 19:30:09 +0000
parents fcec9e0c9c67
children 7447809d31ad
(in-package #:bones.wam)

;;;; Parsing
;;; Turns p(A, q(A, B)) into something like:
;;;
;;;   X0 -> p(X1, X2)
;;;   X1 -> A
;;;   X2 -> q(X1, X3)
;;;   X3 -> B

(defun parse-term (term)
  "Parse a term into a series of register assignments.

  A term is a Lispy representation of the raw Prolog.

  A register assignment is a cons of (register . assigned-to), e.g.:

    (1 . :foo)   ; X1 = Foo
    (2 . (f 1 3) ; X2 = f(X1, X3)

  "
  (labels ((variable-p (term)
             (keywordp term))
           (parse-variable (var registers)
             ;; If we've already seen this variable, just return its position,
             ;; otherwise allocate a register for it.
             (or (position var registers)
                 (vector-push-extend var registers)))
           (parse-structure (structure registers)
             (let* ((functor (first structure))
                    (arguments (rest structure))
                    (contents (list functor)))
               (prog1
                   (vector-push-extend contents registers)
                 ;; Parse the arguments and splice the results into this cell
                 ;; once we're finished.  The children should handle extending
                 ;; the registers as needed.
                 (nconc contents
                        (mapcar #'(lambda (arg)
                                   (parse arg registers))
                                arguments)))))
           (parse (term registers)
             (cond
               ((variable-p term)
                (parse-variable term registers))
               ;; Wrap bare symbols in a list.  Essentially: foo -> foo/0
               ((symbolp term)
                (parse (list term) registers))
               ((listp term)
                (parse-structure term registers)))))
    (let ((registers (make-array 64 :fill-pointer 0 :adjustable t)))
      (parse term registers)
      (loop :for i :from 0
            :for reg :across registers
            :collect (cons i reg)))))


;;;; Flattening
;;; "Flattening" is the process of turning a series of register assignments into
;;; a sorted sequence appropriate for turning into a series of instructions.
;;;
;;; The order depends on whether we're compiling a query term or a program term.
;;;
;;; It's a stupid name because the assignments are already flattened as much as
;;; they ever will be.  "Sorting" would be a better name.  Maybe I'll change it
;;; once I'm done with the book.
;;;
;;; Turns:
;;;
;;;   X0 -> p(X1, X2)
;;;   X1 -> A
;;;   X2 -> q(X1, X3)
;;;   X3 -> B
;;;
;;; into something like:
;;;
;;;   X2 -> q(X1, X3), X0 -> p(X1, X2)

(defun variable-assignment-p (ass)
  "Return whether the register assigment is a simple variable assignment.

  E.g. `X1 = Foo` is simple, but `X2 = f(...)` is not.

  "
  (keywordp (cdr ass)))

(defun find-dependencies (registers)
  "Return a list of dependencies amongst the given registers.

  Each entry will be a cons of `(a . b)` if register `a` depends on `b`.

  "
  (mapcan #'(lambda (assignment)
             (if (variable-assignment-p assignment)
               () ; Variable assignments don't depend on anything else
               (destructuring-bind (target . (functor . reqs))
                   assignment
                 (declare (ignore functor))
                 (loop :for req :in reqs
                       :collect (cons req target)))))
          registers))


(defun flatten-query (registers)
  "Flatten the set of register assignments into a minimal set for a query.

  For queries we require that every register is assigned before it is used.

  We also remove the plain old variable assignments because they're not actually
  needed.

  "
  (-<>> registers
    (topological-sort <> (find-dependencies registers) :key #'car)
    (remove-if #'variable-assignment-p <>)))

(defun flatten-program (registers))


;;;; Tokenization
;;; Tokenizing takes a flattened set of assignments and turns it into a stream
;;; of structure assignments and bare registers.
;;;
;;; It turns:
;;;
;;;   X2 -> q(X1, X3), X0 -> p(X1, X2)
;;;
;;; into something like:
;;;
;;;   (X2 = q/2), X1, X3, (X0 = p/2), X1, X2

(defun tokenize-assignments (assignments)
  "Tokenize a flattened set of register assignments into a stream."
  (mapcan #'(lambda (ass)
             (destructuring-bind (register . (functor . arguments)) ass
               ;; Take a single assignment like:
               ;;   X1 = f(a, b, c)         (1 . (f a b c))
               ;;
               ;; And turn it into a stream of tokens:
               ;;   (X1 = f/3), a, b, c     (1 f 3) a b c
               (cons (list register functor (length arguments))
                     arguments)))
          assignments))


;;;; Actions
;;; Once we have a tokenized stream we can generate the list of machine
;;; instructions from it.
;;;
;;; We turn:
;;;
;;;   (X2 = q/2), X1, X3, (X0 = p/2), X1, X2
;;;
;;; into something like:
;;;
;;;   (#'put-structure 2 q 2)
;;;   (#'set-variable 1)
;;;   (#'set-variable 3)
;;;   (#'put-structure 0 p 2)
;;;   (#'set-value 1)
;;;   (#'set-value 2)

(defun generate-actions (tokens)
  "Generate a series of 'machine instructions' from a stream of tokens."
  (let ((seen (list)))
    (flet ((handle-structure (register functor arity)
             (push register seen)
             (list #'put-structure functor arity register))
           (handle-register (register)
             (if (member register seen)
               (list #'set-value register)
               (progn
                 (push register seen)
                 (list #'set-variable register)))))
      (loop :for token :in tokens
            :collect (if (consp token)
                       (apply #'handle-structure token)
                       (handle-register token))))))


;;;; UI
(defun compile-query-term (term)
  "Parse a Lisp term into a series of WAM machine instructions."
  (-> term parse-term flatten-query tokenize-assignments generate-actions))


(defun run (wam instructions)
  "Execute the machine instructions on the given WAM."
  (mapc #'(lambda (action)
            (apply (car action) wam (cdr action)))
        instructions)
  (values))