src/wam/compiler/7-rendering.lisp @ 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.
author Steve Losh <steve@stevelosh.com>
date Sun, 17 Jul 2016 16:49:06 +0000
parents 8a247663fec5
children 3325913a9b16
(in-package #:bones.wam)

;;;; .-,--.           .
;;;;  `|__/ ,-. ,-. ,-| ,-. ,-. . ,-. ,-.
;;;;  )| \  |-' | | | | |-' |   | | | | |
;;;;  `'  ` `-' ' ' `-^ `-' '   ' ' ' `-|
;;;;                                   ,|
;;;;                                   `'

;;; Rendering is the act of taking the friendly list-of-instructions format and
;;; actually converting it to raw-ass bytes and storing it in an array.


(defun check-instruction (opcode arguments)
  (assert (= (length arguments)
             (1- (instruction-size opcode)))
      ()
    "Cannot push opcode ~A with ~D arguments ~S, it requires exactly ~D."
    (opcode-name opcode)
    (length arguments)
    arguments
    (1- (instruction-size opcode))))


(defun code-push-instruction (store opcode arguments address)
  "Push the given instruction into `store` at `address`.

  `arguments` should be a list of `code-word`s.

  Returns how many words were pushed.

  "
  (check-instruction opcode arguments)
  (setf (aref store address) opcode
        (subseq store (1+ address)) arguments)
  (instruction-size opcode))


(defun render-opcode (opcode-designator)
  (ecase opcode-designator
    (:get-structure          +opcode-get-structure+)
    (:get-variable-local     +opcode-get-variable-local+)
    (:get-variable-stack     +opcode-get-variable-stack+)
    (:get-value-local        +opcode-get-value-local+)
    (:get-value-stack        +opcode-get-value-stack+)
    (:put-structure          +opcode-put-structure+)
    (:put-variable-local     +opcode-put-variable-local+)
    (:put-variable-stack     +opcode-put-variable-stack+)
    (:put-value-local        +opcode-put-value-local+)
    (:put-value-stack        +opcode-put-value-stack+)
    (:subterm-variable-local +opcode-subterm-variable-local+)
    (:subterm-variable-stack +opcode-subterm-variable-stack+)
    (:subterm-value-local    +opcode-subterm-value-local+)
    (:subterm-value-stack    +opcode-subterm-value-stack+)
    (:subterm-void           +opcode-subterm-void+)
    (:put-constant           +opcode-put-constant+)
    (:get-constant           +opcode-get-constant+)
    (:subterm-constant       +opcode-subterm-constant+)
    (:get-list               +opcode-get-list+)
    (:put-list               +opcode-put-list+)
    (:get-lisp-object        +opcode-get-lisp-object+)
    (:put-lisp-object        +opcode-put-lisp-object+)
    (:jump                   +opcode-jump+)
    (:call                   +opcode-call+)
    (:dynamic-jump           +opcode-dynamic-jump+)
    (:dynamic-call           +opcode-dynamic-call+)
    (:proceed                +opcode-proceed+)
    (:allocate               +opcode-allocate+)
    (:deallocate             +opcode-deallocate+)
    (:done                   +opcode-done+)
    (:try                    +opcode-try+)
    (:retry                  +opcode-retry+)
    (:trust                  +opcode-trust+)
    (:cut                    +opcode-cut+)))

(defun render-argument (argument)
  (cond
    ;; Ugly choice point args that'll be filled later...
    ((eq +choice-point-placeholder+ argument) 0)

    ;; Bytecode just needs the register numbers.
    ((typep argument 'register) (register-number argument))

    ;; Everything else just gets shoved right into the array.
    (t argument)))

(defun render-bytecode (store instructions start limit)
  "Render `instructions` (a circle) into `store` starting at `start`.

  Bail if ever pushed beyond `limit`.

  Return the total number of code words rendered.

  "
  (let ((previous-jump nil))
    (flet
        ((fill-previous-jump (address)
           (when previous-jump
             (setf (aref store (1+ previous-jump)) address))
           (setf previous-jump address)))
      (loop
        :with address = start

        ;; Render the next instruction
        :for node = (circle-forward instructions)
        :then (or (circle-forward node)
                  (return instruction-count))

        :for (opcode-designator . arguments) = (circle-value node)
        :for opcode = (render-opcode opcode-designator)
        :for size = (instruction-size opcode)
        :summing size :into instruction-count

        ;; Make sure we don't run past the end of our section.
        :when (>= (+ size address) limit)
        :do (error "Code store exhausted, game over.")

        :do (code-push-instruction store
                                   opcode
                                   (mapcar #'render-argument arguments)
                                   address)

        ;; We need to fill in the addresses for the choice point jumping
        ;; instructions.  For example, when we have TRY ... TRUST, the TRUST
        ;; needs to patch its address into the TRY instruction.
        ;;
        ;; I know, this is ugly, sorry.
        :when (member opcode-designator '(:try :retry :trust))
        :do (fill-previous-jump address)

        ;; look, don't judge me, i told you i know its bad
        :do (incf address size)))))


(defun render-query-into (storage instructions)
  (render-bytecode storage instructions 0 +maximum-query-size+))


(defun mark-label (wam functor arity address)
  "Set the code label `functor`/`arity` to point at `address`."
  (setf (wam-code-label wam functor arity)
        address))

(defun render-rules (wam functor arity instructions)
  ;; Before we render the instructions, make the label point at where they're
  ;; about to go.
  (mark-label wam functor arity (wam-code-pointer wam))
  (incf (wam-code-pointer wam)
        (render-bytecode (wam-code wam)
                         instructions
                         (wam-code-pointer wam)
                         (array-total-size (wam-code wam)))))