// -*- Mode: Lisp; -*- // This file is frules.lisp // Last edited: 7/12/93, by KDF // Copyright (c) 1988-1991, Kenneth D. Forbus, Northwestern University, // and Johan de Kleer, the 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. "(in-package common-lisp-user)"; #f; define class () slot rule-id, init-keyword: #"rule-id"; // unique "name" slot rule-dbclass, init-keyword: #"rule-dbclass"; // Dbclass it is linked to. slot rule-matcher, init-keyword: #"rule-matcher"; // procedure that performs the match. slot rule-body, init-keyword: #"rule-body"; // procedure that does the rule's work. slot rule-assumption?, init-keyword: #"rule-assumption?"; end class ; // Does it make an assumption? define method ftre-rule-printer (r, st, ignore) format(st, "", r.rule-id); end method ftre-rule-printer; define variable *file-counter* = 0; define variable *file-prefix* = ""; // LTD: No macros. #"rule-file"; // Building and installing rules // LTD: No macros. #"rule"; // LTD: No macros. #"a-rule"; // Restriction: An a-rule can have only one trigger! define method parse-triggers (trigger-list) if (empty?(trigger-list)) #f; else let (var, test, new-triggers) = parse-trigger-options(tail(trigger-list), #f, #f); pair(list(head(trigger-list), var, test), parse-triggers(new-triggers)); end if; end method parse-triggers; define method parse-trigger-options (triggers, var, test) select (car(triggers)) #"var" => parse-trigger-options(tail(tail(triggers)), second(triggers), test); #"test" => parse-trigger-options(tail(tail(triggers)), var, second(triggers)); otherwise => values(var, test, triggers); end select; end method parse-trigger-options; define method do-rule (triggers, body, asn?) fluid-bind (*rule-procedures* = #f) fluid-bind (*bound-vars* = #f) let index-form = #f; if (asn? & tail(triggers)) error("\n a-rules can only have one trigger:\n%S,\n%S.", triggers, body); end if; index-form := build-rule(head(triggers), replace-in-tree(#"internal-rule", #"rule", make-nested-rule(tail(triggers), body)), asn?); // Returning this ensures that all procedure definitions // are executed before any indexing occurs. pair(#"progn", concatenate(*rule-procedures*, list(index-form))); end fluid-bind; end fluid-bind; end method do-rule; // LTD: No macros. #"internal-rule"; define method make-nested-rule (triggers, body) if (empty?(triggers)) body; else list(list(#"add-internal-rule", head(triggers), make-nested-rule(tail(triggers), body))); end if; end method make-nested-rule; // LTD: No macros. #"add-internal-rule"; // Building a rule define method build-rule (trigger, body, asn?) let (pattern, var, test) = parse-rule-trigger(trigger); match-procedure := generate-match-procedure(pattern, var, test); body-procedure := generate-body-procedure(pattern, var, body); push!(match-procedure, *rule-procedures*); push!(body-procedure, *rule-procedures*); list(#"insert-rule", apply(list, #"get-dbclass", get-trigger-dbclass(pattern), #(#"*ftre*")), // return form to index rule #(#"function", // the match procedure for rule #(#(#","), #"if", #"*bound-vars*", #(#"list", #(#"quote", #"lambda"), #(#"quote", #(#"p")), #(#"list*", #(#"cadr", #"match-procedure"), #(#"quote", #"p"), #"*bound-vars*")), #(#"cadr", #"match-procedure"))), #(#"function", // the body procedure #(#(#","), #"if", #"*bound-vars*", #(#"let", #(#(#"tv", #(#"nreverse", #(#"pattern-free-variables", #"trigger")))), #(#"list", #(#"quote", #"lambda"), #"tv", #(#"cons", #(#"cadr", #"body-procedure"), #(#"append", #"tv", #(#"quote", #(// (fn-name parameters) #(#(#",@"), #"scratchout", #"tv", #"*bound-vars*"))))))), #(#"cadr", #"body-procedure"))), asn?); end method build-rule; define method parse-rule-trigger (trigger) // A trigger has the form ( ) // where 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. if (variable?(trigger)) trigger; elseif (instance?(trigger, )) apply(values, trigger); else error("Invalid expression in trigger: %S.", trigger); end if; end method parse-rule-trigger; define method get-trigger-dbclass (trigger) if (variable?(trigger)) if (member?(trigger, *bound-vars*)) trigger; else error("\nTrigger dbclass is unbound -- %S.", trigger); end if; elseif (not(instance?(trigger, ))) list(#"quote", trigger); else get-trigger-dbclass(head(trigger)); end if; end method get-trigger-dbclass; // Generating the body procedure // Macro for generate-body-procedure // (macros must be defined before use or compiler dies) // LTD: No macros. #"with-pushed-variable-bindings"; define method generate-body-procedure (pattern, var, body) newly-bound := pattern-free-variables(pattern); if (var) push!(var, newly-bound); end if; body := with-pushed-variable-bindings(newly-bound, fully-expand-body(body)); env := concatenate(newly-bound, scratchout(newly-bound, *bound-vars*)); apply(list, #"defun", generate-rule-procedure-name(pattern), env, body); end method generate-body-procedure; define method generate-match-procedure (pattern, var, test) let (tests, binding-specs) = // Construct a defun specialized to match the given pattern. // That procedure will return NIL if no match, // (values T ) if match is successful. generate-match-body(pattern, pattern-free-variables(pattern), test); apply(list, #"defun", generate-rule-procedure-name(pattern), pair(#"p", *bound-vars*), #(// first arg, P, is the pattern #(#"if", #(#"and", #(#(#",@") . #"tests")), #(#"values", #"t", #(#(#","), #"if", #(#"and", #(#"null", #"var"), #(#"null", #"binding-specs")), #(), #(#"cons", #(#"quote", #"list"), #(#"append", #(#"if", #"var", #(#"quote", #(#"p"))), #(#"reverse", #"binding-specs")))))))); end method generate-match-procedure; define method scratchout (l1, l2) // non-destructive and order-preserving for (el1 in l1) l2 := remove(l2, el1); finally l2; end for; end method scratchout; define method generate-rule-procedure-name (pattern) as(, format(#f, "%S-%S-%S", *file-prefix*, pattern, inc!(*file-counter*))); end method generate-rule-procedure-name; // Recursive macroexpansion #f; *macros-to-expand* := #(#"rule", #"a-rule", #"rlet", #"rassert!", #"internal-rule", #"add-internal-rule", #"with-pushed-variable-bindings"); define method fully-expand-body (body) if (empty?(body)) #f; elseif (~ instance?(body, )) body; elseif (instance?(head(body), )) if (member?(head(body), *macros-to-expand*)) fully-expand-body(// LTD: Function MACROEXPAND not yet implemented. macroexpand(body)); else pair(head(body), fully-expand-body(tail(body))); end if; else pair(fully-expand-body(head(body)), fully-expand-body(tail(body))); end if; end method fully-expand-body; // Running rules define method insert-rule (dbclass, matcher, body, asn?) with-ftre(dbclass-ftre(dbclass), rule := make-rule(matcher: matcher, body: body, dbclass: dbclass, id: inc!(ftre-rule-counter(*ftre*)), assumption?: asn?), // Index it if (ftre-depth(*ftre*) = 0) push!(rule, dbclass-rules(dbclass)); else push!(rule, ftre-local-rules(*ftre*)); end if, for (candidate in get-candidates(dbclass-name(dbclass), *ftre*)) try-rule-on(rule, candidate); end for); end method insert-rule; define method try-rules (fact, ftre) for (rule in get-candidate-rules(fact, ftre)) try-rule-on(rule, fact); end for; end method try-rules; define method get-candidate-rules (fact, ftre) concatenate(ftre-local-rules(ftre), dbclass-rules(get-dbclass(fact, ftre))); end method get-candidate-rules; define method try-rule-on (rule, fact) with-ftre(dbclass-ftre(rule.rule-dbclass), let (okay?, bindings) = (rule.rule-matcher)(fact); if (okay?) enqueue(*ftre*, pair(rule.rule-body, bindings), rule.rule-assumption?); end if); end method try-rule-on; define method run-rules (*ftre*) for (form = dequeue(*ftre*) then dequeue(*ftre*), counter = 0 then 1+(counter), until empty?(form)) inc!(ftre-rules-run(*ftre*)); with-ftre(*ftre*, // Compare this to regular TRE! apply(head(form), tail(form))); finally debugging-ftre("~% ~A(~A): ~A rules run.", *ftre*, ftre-depth(*ftre*), counter); end for; end method run-rules; define method enqueue (ftre, new, asn?) if (asn?) push!(new, ftre-asn-queue(ftre)); else push!(new, ftre-normal-queue(ftre)); end if; end method enqueue; define method dequeue (ftre) if (ftre-normal-queue(ftre)) pop!(ftre-normal-queue(ftre)); else pop!(ftre-asn-queue(ftre)); end if; end method dequeue; // Displaying rules define method show-rules (#key stream = *standard-output*) counter := 0; format(stream, "\n In global context:"); let tab94009 = ftre-dbclass-table(*ftre*); do(method (key, dbclass) for (rule in dbclass-rules(dbclass)) inc!(counter); print-rule(rule, stream); end for; end method, key-sequence(tab94009), tab94009); format(stream, "\n %D global rules.", counter); if (ftre-depth(*ftre*) > 0) format(stream, "\n In current context:"); for (rule in reverse(ftre-local-rules(*ftre*))) if (~ instance?(rule, )) inc!(counter); end if; print-rule(rule, stream); end for; end if; counter; end method show-rules; define method print-rule (rule, #key stream = *standard-output*) format(stream, "\n %S(%S): %S, %S", rule, if (rule.rule-assumption?) "Y"; else "N"; end if, rule.rule-matcher, rule.rule-body); end method print-rule; define method get-rule (id, #key ftre = *ftre*) block (return-from-get-rule) let tab94009 = ftre-dbclass-table(ftre); do(method (key, dbclass) for (rule in dbclass-rules(dbclass)) if (rule.rule-id = id) return-from-get-rule(rule); end if; end for; end method, key-sequence(tab94009), tab94009); end block; end method get-rule;