;; -*- Mode: Lisp; -*- ;;;; Rule package for LTRE ;;;; Last Edited 4/27/95, by KDF ;;; Copyright (c) 1986 - 1995 Kenneth D. Forbus, ;;; Northwestern University, and Johan de Kleer, Xerox Corporation. ;;; All rights reserved. ;;; See the file legal.txt for a paragraph stating scope of permission ;;; and disclaimer of warranty. The above copyright notice and that ;;; paragraph must be included in any separate copy of this file. ;;; The RULES field of the LTRE no longer exists, since (in-package :COMMON-LISP-USER) (proclaim '(special *LTRE*)) (defstruct (rule (:PRINT-FUNCTION rule-print-procedure)) counter ; Unique ID for easy lookup ltre ; The LTRE it is part of dbclass ; Dbclass of associated pattern matcher ; Procedure that performs the match body) ; Procedure that does the rules' work (defun rule-print-procedure (r st ignore) (declare (ignore ignore)) (format st "" (rule-counter r))) ;;; The next three variable are used during rule construction. (defvar *bound-vars* nil) ;; Keeps track of lexical envirionment (defvar *end-forms* nil) ;; Caches procedure definitions (defvar *rule-indexing* nil) ;; Caches rule indexing forms. ;; For adding a file-specific prefix to internal procedure names. (defvar *file-counter* 0) (defvar *file-prefix* "") (defmacro Rule-File (prefix) `(eval-when (compile load eval) (setq *file-counter* 0) (setq *file-prefix* ,prefix))) ;;;; Building and installing rules (defmacro rule (triggers &rest body) (do-rule triggers body)) (defun do-rule (triggers body) (let ((*end-forms* nil) ;; accumulates procedure definitions (*rule-indexing* nil) ;; accumulates indexing forms (*LTRE* *LTRE*)) ;; The LTRE the rule will use ;; Uncomment these if you have a serious rule expansion problem to track down. ;;(format t "~% Adding rule with triggers: ") (pprint triggers) ;;(format t "~% And body: ") (pprint body) (add-rule (car triggers) (subst 'internal-rule 'rule (make-nested-rule (cdr triggers) body))) ;; Returning this ensures that all procedure definitions ;; are executed before any indexing occurs. `(progn ,@ *end-forms* ,@ *rule-indexing*))) (defmacro internal-rule (triggers &rest body) ;; All but the rule corresponding to the outermost ;; trigger are internal rules. `(add-internal-rule ,(car triggers) ,(make-nested-rule (cdr triggers) body))) (defun make-nested-rule (triggers body) (cond ((null triggers) body) (t `((add-internal-rule ,(car triggers) ,(make-nested-rule (cdr triggers) body)))))) (defun add-rule (trigger body) ;; Must be executed after functions are defined. (push (build-rule trigger body) *rule-indexing*) nil) (defmacro add-internal-rule (trigger body) ;; The form to index this rule must appear in ;; the body of the rule which directly contains it. (build-rule trigger body)) ;;;; Building a rule involves analyzing the trigger to see what ;; special-purpose unifier is needed, constructing a function ;; to do the work of the body, and building a form to index it. (defun build-rule (trigger body &aux match-procedure body-procedure) (multiple-value-bind (pattern condition var test) (parse-rule-trigger trigger) (setq match-procedure (generate-match-procedure pattern var test condition)) (setq body-procedure (generate-body-procedure pattern condition var body)) (push match-procedure *end-forms*) (push body-procedure *end-forms*) `(insert-rule (get-dbclass ,(get-trigger-dbclass pattern)) ;return form to index rule (function ;the match function for rule ,(if *bound-vars* `(lambda (p) (,(cadr match-procedure) p ,@ *bound-vars*)) (cadr match-procedure))) (function ;;the body function for rule ,(if (or *bound-vars* (not (eq condition :INTERN))) (let ((tv (nreverse (pattern-free-variables trigger)))) (unless (eq condition :INTERN) (push 'TRIGGER-NODE tv)) `(lambda ,tv (,(cadr body-procedure) ,@ tv ;(fn-name parameters) ,@ (scratchout tv *bound-vars*)))) (cadr body-procedure)))))) (defun parse-rule-trigger (trigger) ;; Trigger syntax is now ( . ) ;; where = :INTERN | :TRUE | :FALSE ;; and can be empty, or ;; :TEST and/or :VAR , where must be ;; non-nil for the match to succeed, and will be ;; bound to the whole pattern. (values (cadr trigger) (cond ((member (car trigger) '(:INTERN :TRUE :FALSE)) (car trigger)) (t (error "~% Unknown belief condition ~A in trigger ~A." (car trigger) trigger))) (cadr (member :VAR (cddr trigger))) (cadr (member :TEST (cddr trigger))))) (defun get-trigger-dbclass (trigger) (cond ((variable? trigger) (if (member trigger *bound-vars*) trigger (error "~%Trigger dbclass is unbound -- ~A." trigger))) ((atom trigger) (list 'QUOTE trigger)) (t (get-trigger-dbclass (car trigger))))) ;;;; Generating the body function ;;; Macro for generate-body-procedure ;;; (macros must be defined before use or compiler dies) (defmacro with-pushed-variable-bindings (new-bindings &rest body) `(let ((*bound-vars* (append ,new-bindings (scratchout ,new-bindings *bound-vars*)))) ,@ body)) (defun generate-body-procedure (pattern condition var body &aux newly-bound env fname) (setq newly-bound (pattern-free-variables pattern)) (if var (push var newly-bound)) (setq body (with-pushed-variable-bindings newly-bound (fully-expand-body body))) (setq env (append newly-bound (scratchout newly-bound *bound-vars*))) (unless (eq condition :INTERN) (push 'trigger-node env)) (setq fname (generate-rule-procedure-name pattern)) `(defun ,fname ,env ,@ (cond ((eq condition :INTERN) body) ;; Just do it (t ;; Must check and see if the node's belief state ;; matches the rule's requirements `((cond ((,(ecase condition (:TRUE 'true-node?)(:FALSE 'false-node?)) TRIGGER-NODE) ,@ body) (t (push (list ',fname ,@ env) ,(ecase condition (:TRUE `(tms-node-true-rules TRIGGER-NODE)) (:FALSE `(tms-node-false-rules TRIGGER-NODE))))))))))) (defun generate-match-procedure (pattern var test condition) (multiple-value-bind (tests binding-specs) ;;make special tests to check for this pattern (generate-match-body pattern (pattern-free-variables pattern) (if var (list (cons var 'P))) test) ;; Seed with :VAR binding `(defun ,(generate-rule-procedure-name pattern) (P ,@ *bound-vars*) ;;first arg, P, is the pattern (if (and ,@ tests) (values T (list ,@ (if var '(P)) ,@ (reverse binding-specs)) ,(unless (eq condition :INTERN) t)))))) (defun scratchout (l1 l2) ;non-destructive and order-preserving (dolist (el1 l1 l2) (setq l2 (remove el1 l2)))) (defun generate-rule-procedure-name (pattern) (intern (format nil "~A-~A-~A" *file-prefix* pattern (incf *file-counter*)))) ;;;; Recursive macroexpansion (defvar *macros-to-expand* '(rule internal-rule add-internal-rule with-pushed-variable-bindings rlet rassert! rretract! assuming with-LTRE with-contradiction-handler with-contradiction-check without-contradiction-check)) (defun fully-expand-body (body) ;; New version -- only expands when it is clear that expansion is needed. (cond ((null body) nil) ((not (listp body)) body) ((symbolp (car body)) (cond ((member (car body) *macros-to-expand*) (fully-expand-body (macroexpand body))) (t (cons (car body) (fully-expand-body (cdr body)))))) (t (cons (fully-expand-body (car body)) (fully-expand-body (cdr body)))))) ;;;; Running rules (defun insert-rule (dbclass matcher body &aux rule *LTRE*) (setq *LTRE* (dbclass-ltre dbclass)) (setq rule (make-rule :MATCHER matcher :BODY body :DBCLASS dbclass :COUNTER (incf (ltre-rule-counter *LTRE*)))) ;; Index it (push rule (dbclass-rules dbclass)) (dolist (candidate (dbclass-facts dbclass)) (try-rule-on rule candidate))) (defun try-rules (datum) (dolist (rule (dbclass-rules (datum-dbclass datum))) (try-rule-on rule datum))) (defun try-rule-on (rule datum &aux *LTRE*) (setq *LTRE* (datum-ltre datum)) (multiple-value-bind (okay? bindings node?) (funcall (rule-matcher rule) (datum-lisp-form datum)) (when okay? (when node? (push (datum-tms-node datum) bindings)) (enqueue (cons (rule-body rule) bindings) *LTRE*)))) (defun run-rules (&optional (*LTRE* *LTRE*)) (do ((form (dequeue *LTRE*) (dequeue *LTRE*)) (counter 0)) ((null form) (debugging-ltre "~% ~A rules run." counter) (incf (ltre-rules-run *LTRE*) counter) counter) (apply (car form) (cdr form)) (incf counter))) (defun run-one-rule (&optional (*LTRE* *LTRE*)) (let ((rule (dequeue *LTRE*))) (when rule (debugging-ltre "~% Executing single rule.") (incf (ltre-rules-run *LTRE*)) (apply (car rule) (cdr rule))) (rules-waiting? *LTRE*))) (defun rules-waiting? (&optional (*LTRE* *LTRE*)) (not (null (ltre-queue *LTRE*)))) (defun enqueue (new *LTRE*) (push new (ltre-queue *LTRE*))) (defun dequeue (ltre) (pop (ltre-queue ltre))) ;;;; Display routines (defun show-rules (&optional (*LTRE* *LTRE*) (stream *standard-output*) &aux counter) (setq counter 0) (format t "~%The rules in ~A are:" (ltre-title *LTRE*)) (map-dbclass #'(lambda (dbclass) (dolist (rule (dbclass-rules dbclass)) (incf counter) (print-rule rule stream)))) counter) (defun print-rule (rule &optional (stream *standard-output*)) (format stream "~% ~A: ~A, ~A" rule (rule-matcher rule) (rule-body rule))) (defun test-rule-expansion () (pprint (macroexpand '(rule ((:TRUE (foo ?p ?q) :VAR ?f1) (:FALSE (bar ?q ?r) :VAR ?f2) (:INTERN (interesting ?r) :VAR ?f3)) (rassert! (:TAXONOMY ?p ?q ?f3) (:RANDOM-TEST ?f1 ?f2)))))) (defun get-rule (num &optional (*LTRE* *LTRE*)) (dolist (rule (ltre-rules *LTRE*)) (when (= (rule-counter rule) num) (return-from GET-RULE rule))))