ba96e98a1d54

Add precompilation of static queries at compile time

Imagine a function like this:

    (defun legal-moves ()
      (query (legal ?who ?move)))

The argument to `query` there is constant, so we can compile it into WAM
bytecode once, when the Lisp function around it is compiled.  Then running the
query doesn't need to touch the Bones compiler -- it can just load the bytecode
from an array and first up the VM loop.

This saves a lot of time (and consing) compared to compiling the same query over
and over at runtime.
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Sun, 17 Jul 2016 16:49:06 +0000
parents aacf9ee4fddc
children 582f7076626b
branches/tags (none)
files .lispwords src/make-quickutils.lisp src/quickutils.lisp src/wam/compiler/7-rendering.lisp src/wam/compiler/8-ui.lisp src/wam/types.lisp src/wam/ui.lisp src/wam/vm.lisp src/wam/wam.lisp

Changes

--- a/.lispwords	Sun Jul 17 00:50:25 2016 +0000
+++ b/.lispwords	Sun Jul 17 16:49:06 2016 +0000
@@ -8,3 +8,4 @@
 (0 push-logic-frame-with)
 (1 cell-typecase)
 (1 opcode-case)
+(2 define-invocation)
--- a/src/make-quickutils.lisp	Sun Jul 17 00:50:25 2016 +0000
+++ b/src/make-quickutils.lisp	Sun Jul 17 16:49:06 2016 +0000
@@ -5,6 +5,7 @@
   :utilities '(:define-constant
                :set-equal
                :curry
+               :rcurry
                :switch
                :ensure-boolean
                :while
--- a/src/quickutils.lisp	Sun Jul 17 00:50:25 2016 +0000
+++ b/src/quickutils.lisp	Sun Jul 17 16:49:06 2016 +0000
@@ -2,7 +2,7 @@
 ;;;; See http://quickutil.org for details.
 
 ;;;; To regenerate:
-;;;; (qtlc:save-utils-as "quickutils.lisp" :utilities '(:DEFINE-CONSTANT :SET-EQUAL :CURRY :SWITCH :ENSURE-BOOLEAN :WHILE :UNTIL :TREE-MEMBER-P :TREE-COLLECT :WITH-GENSYMS :ONCE-ONLY :ZIP :ALIST-TO-HASH-TABLE :MAP-TREE :WEAVE :RANGE :ALIST-PLIST :EQUIVALENCE-CLASSES :MAP-PRODUCT) :ensure-package T :package "BONES.QUICKUTILS")
+;;;; (qtlc:save-utils-as "quickutils.lisp" :utilities '(:DEFINE-CONSTANT :SET-EQUAL :CURRY :RCURRY :SWITCH :ENSURE-BOOLEAN :WHILE :UNTIL :TREE-MEMBER-P :WITH-GENSYMS :ONCE-ONLY :ZIP :ALIST-TO-HASH-TABLE :MAP-TREE :WEAVE :ALIST-PLIST :EQUIVALENCE-CLASSES :MAP-PRODUCT) :ensure-package T :package "BONES.QUICKUTILS")
 
 (eval-when (:compile-toplevel :load-toplevel :execute)
   (unless (find-package "BONES.QUICKUTILS")
@@ -15,13 +15,12 @@
 (when (boundp '*utilities*)
   (setf *utilities* (union *utilities* '(:DEFINE-CONSTANT :SET-EQUAL
                                          :MAKE-GENSYM-LIST :ENSURE-FUNCTION
-                                         :CURRY :STRING-DESIGNATOR
+                                         :CURRY :RCURRY :STRING-DESIGNATOR
                                          :WITH-GENSYMS :EXTRACT-FUNCTION-NAME
                                          :SWITCH :ENSURE-BOOLEAN :UNTIL :WHILE
-                                         :TREE-MEMBER-P :TREE-COLLECT
-                                         :ONCE-ONLY :TRANSPOSE :ZIP
-                                         :ALIST-TO-HASH-TABLE :MAP-TREE :WEAVE
-                                         :RANGE :SAFE-ENDP :ALIST-PLIST
+                                         :TREE-MEMBER-P :ONCE-ONLY :TRANSPOSE
+                                         :ZIP :ALIST-TO-HASH-TABLE :MAP-TREE
+                                         :WEAVE :SAFE-ENDP :ALIST-PLIST
                                          :EQUIVALENCE-CLASSES :MAPPEND
                                          :MAP-PRODUCT))))
 
@@ -118,6 +117,16 @@
            (apply ,fun ,@curries more)))))
   
 
+  (defun rcurry (function &rest arguments)
+    "Returns a function that applies the arguments it is called
+with and `arguments` to `function`."
+    (declare (optimize (speed 3) (safety 1) (debug 1)))
+    (let ((fn (ensure-function function)))
+      (lambda (&rest more)
+        (declare (dynamic-extent more))
+        (multiple-value-call fn (values-list more) (values-list arguments)))))
+  
+
   (deftype string-designator ()
     "A string designator type. A string designator is either a string, a symbol,
 or a character."
@@ -240,25 +249,6 @@
       (rec tree)))
   
 
-  (defun tree-collect (predicate tree)
-    "Returns a list of every node in the `tree` that satisfies the `predicate`. If there are any improper lists in the tree, the `predicate` is also applied to their dotted elements."
-    (let ((sentinel (gensym)))
-      (flet ((my-cdr (obj)
-               (cond ((consp obj)
-                      (let ((result (cdr obj)))
-                        (if (listp result)
-                            result
-                            (list result sentinel))))
-                     (t
-                      (list sentinel)))))
-        (loop :for (item . rest) :on tree :by #'my-cdr
-              :until (eq item sentinel)
-              :if (funcall predicate item) collect item
-                :else
-                  :if (listp item)
-                    :append (tree-collect predicate item)))))
-  
-
   (defmacro once-only (specs &body forms)
     "Evaluates `forms` with symbols specified in `specs` rebound to temporary
 variables, ensuring that each initform is evaluated only once.
@@ -337,14 +327,6 @@
     (apply #'mapcan #'list lists))
   
 
-  (defun range (start end &key (step 1) (key 'identity))
-    "Return the list of numbers `n` such that `start <= n < end` and
-`n = start + k*step` for suitable integers `k`. If a function `key` is
-provided, then apply it to each number."
-    (assert (<= start end))
-    (loop :for i :from start :below end :by step :collecting (funcall key i)))
-  
-
   (declaim (inline safe-endp))
   (defun safe-endp (x)
     (declare (optimize safety))
@@ -423,9 +405,9 @@
       (%map-product (ensure-function function) (cons list more-lists))))
   
 (eval-when (:compile-toplevel :load-toplevel :execute)
-  (export '(define-constant set-equal curry switch eswitch cswitch
-            ensure-boolean while until tree-member-p tree-collect with-gensyms
+  (export '(define-constant set-equal curry rcurry switch eswitch cswitch
+            ensure-boolean while until tree-member-p with-gensyms
             with-unique-names once-only zip alist-to-hash-table map-tree weave
-            range alist-plist plist-alist equivalence-classes map-product)))
+            alist-plist plist-alist equivalence-classes map-product)))
 
 ;;;; END OF quickutils.lisp ;;;;
--- a/src/wam/compiler/7-rendering.lisp	Sun Jul 17 00:50:25 2016 +0000
+++ b/src/wam/compiler/7-rendering.lisp	Sun Jul 17 16:49:06 2016 +0000
@@ -132,8 +132,8 @@
         :do (incf address size)))))
 
 
-(defun render-query (wam instructions)
-  (render-bytecode (wam-code wam) instructions 0 +maximum-query-size+))
+(defun render-query-into (storage instructions)
+  (render-bytecode storage instructions 0 +maximum-query-size+))
 
 
 (defun mark-label (wam functor arity address)
--- a/src/wam/compiler/8-ui.lisp	Sun Jul 17 00:50:25 2016 +0000
+++ b/src/wam/compiler/8-ui.lisp	Sun Jul 17 16:49:06 2016 +0000
@@ -8,19 +8,33 @@
 
 ;;; The final phase wraps everything else up into a sane UI.
 
+(defun %compile-query-into (storage query)
+  (multiple-value-bind (instructions permanent-variables)
+      (precompile-query query)
+    (optimize-instructions instructions)
+    (values permanent-variables
+            (render-query-into storage instructions))))
+
 (defun compile-query (wam query)
   "Compile `query` into the query section of the WAM's code store.
 
   `query` should be a list of goal terms.
 
-  Returns the permanent variables.
+  Returns the permanent variables and the size of the compiled bytecode.
 
   "
-  (multiple-value-bind (instructions permanent-variables)
-      (precompile-query query)
-    (optimize-instructions instructions)
-    (render-query wam instructions)
-    permanent-variables))
+  (%compile-query-into (wam-code wam) query))
+
+(defun compile-query-into (storage query)
+  "Compile `query` into the given array `storage`.
+
+  `query` should be a list of goal terms.
+
+  Returns the permanent variables and the size of the compiled bytecode.
+
+  "
+  (%compile-query-into storage query))
+
 
 (defun compile-rules (wam rules)
   "Compile `rules` into the WAM's code store.
--- a/src/wam/types.lisp	Sun Jul 17 00:50:25 2016 +0000
+++ b/src/wam/types.lisp	Sun Jul 17 16:49:06 2016 +0000
@@ -54,6 +54,9 @@
 (deftype query-code-holder ()
   `(simple-array code-word (,+maximum-query-size+)))
 
+(deftype query-size ()
+  `(integer 0 ,+maximum-query-size+))
+
 (deftype instruction-size ()
   `(integer 1 ,+maximum-instruction-size+))
 
--- a/src/wam/ui.lisp	Sun Jul 17 00:50:25 2016 +0000
+++ b/src/wam/ui.lisp	Sun Jul 17 16:49:06 2016 +0000
@@ -21,20 +21,21 @@
 
 
 ;;;; Normalization
-(defun normalize-term (term)
-  ;; Normally a rule consists of a head terms and multiple body terms, like so:
-  ;;
-  ;;     (likes sally ?who) (likes ?who cats)
-  ;;
-  ;; But sometimes people are lazy and don't include the parens around
-  ;; zero-arity predicates:
-  ;;
-  ;;     (happy steve) sunny
-  (if (and (not (variablep term))
-           (symbolp term)
-           (not (eq term '!))) ; jesus
-    (list term)
-    term))
+(eval-when (:compile-toplevel :load-toplevel :execute)
+  (defun normalize-term (term)
+    ;; Normally a rule consists of a head terms and many body terms, like so:
+    ;;
+    ;;     (likes sally ?who) (likes ?who cats)
+    ;;
+    ;; But sometimes people are lazy and don't include the parens around
+    ;; zero-arity predicates:
+    ;;
+    ;;     (happy steve) sunny
+    (if (and (not (variablep term))
+             (symbolp term)
+             (not (eq term '!))) ; jesus
+      (list term)
+      term)))
 
 
 ;;;; Assertion
@@ -86,58 +87,78 @@
 
 
 ;;;; Querying
+(defun perform-aot-query (code size vars result-function)
+  (assert *database* (*database*) "No database.")
+  (run-aot-compiled-query *database* code size vars
+                          :result-function result-function))
+
 (defun perform-query (terms result-function)
   (assert *database* (*database*) "No database.")
   (run-query *database* (mapcar #'normalize-term terms)
              :result-function result-function))
 
 
-(defun invoke-query (&rest terms)
+(defmacro define-invocation ((name aot-name) arglist &body body)
+  (with-gensyms (terms data code size vars)
+    `(progn
+      (defun ,name ,(append arglist `(&rest ,terms))
+        (macrolet ((invoke (result-function)
+                     `(perform-query ,',terms ,result-function)))
+          ,@body))
+      (defun ,aot-name ,(append arglist `(,data))
+        (destructuring-bind (,code ,size ,vars) ,data
+          (macrolet ((invoke (result-function)
+                       `(perform-aot-query ,',code ,',size ,',vars
+                                           ,result-function)))
+            ,@body))))))
+
+
+(define-invocation (invoke-query invoke-query-aot) ()
   (let ((result nil)
         (succeeded nil))
-    (perform-query terms (lambda (r)
-                           (setf result r
-                                 succeeded t)
-                           t))
+    (invoke (lambda (r)
+              (setf result r
+                    succeeded t)
+              t))
     (values result succeeded)))
 
-(defun invoke-query-all (&rest terms)
+(define-invocation (invoke-query-all invoke-query-all-aot) ()
   (let ((results nil))
-    (perform-query terms (lambda (result)
-                           (push result results)
-                           nil))
-    (nreverse results)))
-
-(defun invoke-query-map (function &rest terms)
-  (let ((results nil))
-    (perform-query terms (lambda (result)
-                           (push (funcall function result) results)
-                           nil))
+    (invoke (lambda (result)
+              (push result results)
+              nil))
     (nreverse results)))
 
-(defun invoke-query-do (function &rest terms)
-  (perform-query terms (lambda (result)
-                         (funcall function result)
-                         nil))
+(define-invocation (invoke-query-map invoke-query-map-aot) (function)
+  (let ((results nil))
+    (invoke (lambda (result)
+              (push (funcall function result) results)
+              nil))
+    (nreverse results)))
+
+(define-invocation (invoke-query-do invoke-query-do-aot) (function)
+  (invoke (lambda (result)
+            (funcall function result)
+            nil))
   (values))
 
-(defun invoke-query-find (predicate &rest terms)
+(define-invocation (invoke-query-find invoke-query-find-aot) (predicate)
   (let ((results nil)
         (succeeded nil))
-    (perform-query terms (lambda (result)
-                           (if (funcall predicate result)
-                             (progn (setf results result
-                                          succeeded t)
-                                    t)
-                             nil)))
+    (invoke (lambda (result)
+              (if (funcall predicate result)
+                (progn (setf results result
+                             succeeded t)
+                       t)
+                nil)))
     (values results succeeded)))
 
-(defun invoke-prove (&rest terms)
+(define-invocation (invoke-prove invoke-prove-aot) ()
   (let ((succeeded nil))
-    (perform-query terms (lambda (result)
-                           (declare (ignore result))
-                           (setf succeeded t)
-                           t))
+    (invoke (lambda (result)
+              (declare (ignore result))
+              (setf succeeded t)
+              t))
     succeeded))
 
 
@@ -163,6 +184,39 @@
   `(invoke-prove ,@(quote-terms terms)))
 
 
+;;;; Chili Dogs
+(eval-when (:compile-toplevel :load-toplevel :execute)
+  (defun make-aot-data-form (terms)
+    (with-gensyms (code size vars)
+      `(load-time-value
+        (let* ((,code (allocate-query-holder)))
+          (multiple-value-bind (,vars ,size)
+              (compile-query-into
+                ,code ',(->> terms
+                          (mapcar #'eval)
+                          (mapcar #'normalize-term)))
+            (list ,code ,size ,vars)))
+        t))))
+
+
+(defmacro define-invocation-compiler-macro (name aot-name arglist)
+  `(define-compiler-macro ,name (&whole form
+                                 ,@arglist
+                                 &rest terms
+                                 &environment env)
+    (if (every (rcurry #'constantp env) terms)
+      `(,',aot-name ,,@arglist ,(make-aot-data-form terms))
+      form)))
+
+
+(define-invocation-compiler-macro invoke-query      invoke-query-aot ())
+(define-invocation-compiler-macro invoke-query-all  invoke-query-all-aot ())
+(define-invocation-compiler-macro invoke-query-map  invoke-query-map-aot (function))
+(define-invocation-compiler-macro invoke-query-do   invoke-query-do-aot (function))
+(define-invocation-compiler-macro invoke-query-find invoke-query-find-aot (predicate))
+(define-invocation-compiler-macro invoke-prove      invoke-prove-aot ())
+
+
 ;;;; Debugging
 (defun dump (&optional full-code)
   (dump-wam-full *database*)
--- a/src/wam/vm.lisp	Sun Jul 17 00:50:25 2016 +0000
+++ b/src/wam/vm.lisp	Sun Jul 17 16:49:06 2016 +0000
@@ -873,24 +873,42 @@
             (error "Fell off the end of the program code store."))))
   (values))
 
-(defun run-query (wam term &key (result-function
-                                  (lambda (results)
-                                    (declare (ignore results)))))
-  "Compile query `term` and run the instructions on the `wam`.
 
-  Resets the heap, etc before running.
-
-  When `*step*` is true, break into the debugger before calling the procedure and
-  after each instruction.
-
-  "
-  (let ((vars (compile-query wam term)))
-    (setf (wam-program-counter wam) 0
-          (wam-continuation-pointer wam) +code-sentinel+)
-    (run wam (lambda ()
-               (funcall result-function
-                        (extract-query-results wam vars)))))
+(defun %run-query (wam vars result-function)
+  (setf (wam-program-counter wam) 0
+        (wam-continuation-pointer wam) +code-sentinel+)
+  (run wam (lambda ()
+             (funcall result-function
+                      (extract-query-results wam vars))))
   (wam-reset! wam)
   (values))
 
+(defun run-query (wam terms &key (result-function
+                                   (lambda (results)
+                                     (declare (ignore results)))))
+  "Compile query `terms` and run the instructions on the `wam`.
 
+  Resets the heap, etc after running.
+
+  When `*step*` is true, break into the debugger before calling the procedure
+  and after each instruction.
+
+  "
+  (%run-query wam (compile-query wam terms) result-function))
+
+(defun run-aot-compiled-query (wam query-code query-size query-vars
+                               &key (result-function
+                                      (lambda (results)
+                                        (declare (ignore results)))))
+  "Run the AOT-compiled query `code`/`vars` on the `wam`.
+
+  Resets the heap, etc after running.
+
+  When `*step*` is true, break into the debugger before calling the procedure
+  and after each instruction.
+
+  "
+  (wam-load-query-code! wam query-code query-size)
+  (%run-query wam query-vars result-function))
+
+
--- a/src/wam/wam.lisp	Sun Jul 17 00:50:25 2016 +0000
+++ b/src/wam/wam.lisp	Sun Jul 17 16:49:06 2016 +0000
@@ -10,6 +10,13 @@
     :initial-element 0
     :element-type 'code-word))
 
+(defun allocate-query-holder ()
+  (make-array +maximum-query-size+
+    :adjustable nil
+    :initial-element 0
+    :element-type 'code-word))
+
+
 (defun allocate-wam-type-store (size)
   ;; The main WAM store(s) contain three separate blocks of values:
   ;;
@@ -638,9 +645,12 @@
       (remhash functor atable))))
 
 
-(defun wam-load-query-code! (wam query-code)
-  (setf (subseq (wam-code wam) 0) query-code)
-  (values))
+(declaim (ftype (function (wam query-code-holder query-size)
+                          (values null &optional))
+                wam-load-query-code!))
+(defun wam-load-query-code! (wam query-code query-size)
+  (setf (subseq (wam-code wam) 0 query-size) query-code)
+  nil)
 
 
 ;;;; Logic Stack