#|$ACL2s-Preamble$; ; The Key Tuple Data Structure ; J Strother Moore (include-book "primitive-lemmas") (begin-book t :ttags :all);$ACL2s-Preamble$|# ; Description: ; While lists can be used to represent n-tuples positionally, that simple ; representation becomes confusing when we have lots of different kinds of ; n-tuples, each has many different components, and some components contain ; other kinds of n-tuples. Under those conditions it is hard to look at a ; concrete n-tuple and recognize its pieces. ; To address this I develop a less efficient representation (see [1] below) ; called a "keytuple". Among the advantages of keytuples over positionally ; represented n-tuples are that ; (a) you can look at a concrete one and tell what kind of keytuple it ; represents, ; (b) you can look at a concrete one and identify its components, ; (c) you don't have to remember which component is supplied in which position ; when you construct a new instance of given kind of keytuple, ; (d) different kinds of keytuples can have slots labeled with the same name, ; and ; (e) instead of getting a plethora of accessor functions, we have just one: ; get. ; It was the last two advantages that motivated me to use keytuples in the JVM ; model. The other advantages are pretty much hidden by the modify macro which ; I'll define later. ; So what is a keytuple? A keytuple is a type-tagged binding environment, ; i.e., a pair consisting of the type of record to be represented followed by a ; binding environment associating keys (field names) and values. Field names ; are additionally distinguished by using their keyword versions, i.e., symbols ; which print with a leading colon as do :NAME and :BALANCE. ; In this book I define some ACL2 functions and macros and prove some theorems. ; After including this book into an ACL2 session (and selecting "M5" as the ; current package (see [2])), you can then use: ; (defkeytuple ( ... )) - to declare a new ; type of keytuple with k fields. ; Thereafter you can make instances with a new macro called MAKE, determine the ; record type with the function TYPE, and access and "change" field values with ; GET and PUT. ; Here is how you declare an object called an ACCOUNT with two fields, :NAME ; and :BALANCE. ; (defkeytuple ACCOUNT (:NAME :BALANCE)) ; After doing the above, you can write either of the following to build an ; ACCOUNT object with :NAME "John" and :BALANCE 100: ; (MAKE ACCOUNT :NAME "John" :BALANCE 100) ; (MAKE ACCOUNT :BALANCE 100 :NAME "John") ; The values specified for the fields are evaluated. Thus, you could also ; write ; (MAKE ACCOUNT :NAME "John" :BALANCE (+ 50 50)) ; (MAKE ACCOUNT :BALANCE (+ 50 50) :NAME "John") ; If you have an account object, acct, you can access and change the fields ; with GET and PUT as shown below. ; (GET :NAME acct) - get contents of :NAME field of acct ; (PUT :NAME x acct) - make a new ACCOUNT by changing the :NAME field to x ; (MAKE ACCOUNT ...) is just an abbreviation for a call of the function ACCOUNT ; which takes its arguments positionally. For example, ; (MAKE ACCOUNT :BALANCE 100 :NAME "John") ; is just an abbreviation for: ; (ACCOUNT "John" 100) ; MAKE "knows" the order of the arguments, so you don't have to. If you ask ; MAKE to build an ACCOUNT with a field that was not declared in the ; defkeytuple for ACCOUNT, it will cause an error: ; (MAKE ACCOUNT :NAME "John" :BALANCE 100 :ZIP "12345") ; ACL2 Error in macro expansion: Illegal key/value args (:ZIP "12345") in macro ; expansion of (ACCOUNT-MACRO :NAME "John" :BALANCE 100 :ZIP "12345"). The ; argument list for ACCOUNT-MACRO is (&KEY NAME BALANCE). ; The concrete representation of the ACCOUNT object created by ; (MAKE ACCOUNT :NAME "John" :BALANCE 100) ; is ; (ACCOUNT (:NAME "John") (:BALANCE 100)) ; Thus, if you see a concrete ACCOUNT object you can tell which field is which ; because they are labeled by their keywords. ; You can use defkeytuple to define an object with an arbitrary number of ; fields. ; You are also permitted to write things like: ; (defkeytuple ACCOUNT (:NAME :BALANCE) :allow-but-ignore (:NICKNAME)) ; This defines ACCOUNT just as shown above but allows you to include the ; non-existent field name :NICKNAME without causing an error. So this would ; then be legal: ; (MAKE ACCOUNT :NAME "John" :BALANCE 100 :NICKNAME "Johnny") ; and creates the same object as before. That is, :NICKNAME is just ignored. ; This option is implemented just to permit a very peculiar "trick" later; ; we'll explain then. ; If you have several kinds of keytuples in a system, you will want to ; recognize which are which. You do this with TYPE. Suppose you've done two ; defkeytuples, ; (defkeytuple account (:name :balance)) ; (defkeytuple employee (:name :address :room)) ; Suppose acct is an object created by (MAKE ACCOUNT ...) and empl is an object ; created by (MAKE EMPLOYEE ...). ; Then (TYPE acct) is ACCOUNT and (TYPE empl) is EMPLOYEE. ; While it is tempting to think of keytuples as Objects (in the ``object ; oriented programming'' sense) one should not carry the analogy too far. ACL2 ; is a functional language so our keytuples are immutable. ACL2 is untyped, so ; there are no restrictions about how one might access the substructures of a ; keytuple, i.e., there is no hiding of any information. In addition, any slot ; may be set to contain any ACL2 object. Finally, there is no notion of ; ``inheritance.'' ; We also deal with lists of keytuples. Suppose we have a list of keytuples, lst, ; and that every keytuple in the list has the field named key. ; Here are two common operations. ; (FIND key val lst) - find the first keytuple in lst whose key value is ; val and return that keytuple; nil if no such keytuple ; exists in lst. ; (REPLACE key val new lst) - find the keytuple described above and replace it ; in lst with the keytuple new; if no such keytuple ; exists, just add new to lst. ; There is no reason to read further unless you want to understand how ; defkeytuple works. ; --- ; How It Is Done ; Here is the macroexpansion of our typical keytuple example: ; M5 !>:trans1 (defkeytuple account (name balance)) ; (ENCAPSULATE ; NIL (SET-INHIBIT-WARNINGS "non-rec") ; (DEFUN ACCOUNT (NAME BALANCE) ; (DECLARE (XARGS :GUARD T)) ; (CONS 'ACCOUNT ; (LIST (LIST :NAME NAME) ; (LIST :BALANCE BALANCE)))) ; (DEFMACRO ACCOUNT-MACRO (&KEY NAME BALANCE NICKNAME) ; (DECLARE (IGNORE NICKNAME)) ; (CONS 'ACCOUNT ; (CONS NAME (CONS BALANCE NIL)))) ; (DEFMACRO NAME (X) ; (CONS 'GET (CONS :NAME (CONS X 'NIL)))) ; (DEFMACRO BALANCE (X) ; (CONS 'GET ; (CONS :BALANCE (CONS X 'NIL)))) ; (DEFTHM ACCOUNT-RULES ; (AND (EQUAL (TYPE (ACCOUNT NAME BALANCE)) ; 'ACCOUNT) ; (EQUAL (HAS-KEYP :NAME (ACCOUNT NAME BALANCE)) ; T) ; (EQUAL (HAS-KEYP :BALANCE (ACCOUNT NAME BALANCE)) ; T) ; (EQUAL (GET :NAME (ACCOUNT NAME BALANCE)) ; NAME) ; (EQUAL (GET :BALANCE (ACCOUNT NAME BALANCE)) ; BALANCE) ; (EQUAL (PUT :NAME ; NEW-NAME-VAL (ACCOUNT NAME BALANCE)) ; (ACCOUNT NEW-NAME-VAL BALANCE)) ; (EQUAL (PUT :BALANCE ; NEW-BALANCE-VAL (ACCOUNT NAME BALANCE)) ; (ACCOUNT NAME NEW-BALANCE-VAL))) ; :HINTS (("Goal" :IN-THEORY (ENABLE TYPE HAS-KEYP GET PUT)))) ; (IN-THEORY (DISABLE ACCOUNT))) ; ; ; Consider (defkeytuple account (name balance)). It defines the function ; ACCOUNT and the macro ACCOUNT-MACRO that takes keyword arguments and ; generates a call of ACCOUNT with the arguments in the right positions. It ; defines the macros NAME and BALANCE to look up the values of the ; corresponding keywords. If these fields have already been introduced in some ; other keytuples, then these definitions are just redundant. It then proves a ; collection of :REWRITE rules named ACCOUNT-RULES which explain how TYPE, GET, ; and PUT work on ACCOUNT. For these rules to be useful one should disable ; ACCOUNT, which is the last thing the expansion does. ; Notes ; [1] Despite the relative simplicity and elegance of keytuples, they ; are less efficient that the positional representation. More ; structure has to be created to build one, more structure has to ; be walked over to access each slot, and more structure has to be ; copied when we put a new value into a slot. Nevertheless, we ; will use keytuples to represent states of M5. That is because ; we care more about the perspicuity of the M5 model than we do ; about its runtime efficiency. ; If our goal were to make our formal model of M5 run fast, we ; would not use keytuples! We would use ACL2's ``single threaded ; objects.'' These objects, also known as ``stobjs'' may be ; destructively modified ``under the hood'' provided we follow ; syntactic rules that insure that the axioms of ACL2 cannot ; ``detect'' such non-applicative actions. Using stobjs we ; could make M5 run very fast. See the doc topic stobj. ; In a disciplined development of M5 with the dual goals of supporting ; both perspicuity and fast execution, we could ; (a) use keytuples to present the ``logical'' model, ; (b) use stobjs to implement a ``fast model'', ; (c) prove that the two models are equivalent in a suitable ; sense, and then ; (d) use the MBE device of ACL2 to install the fast model as the ; execution engine for the logical one. ; This would give us the logical clarity and ease of reading of ; keytuples when viewing states in proofs and the execution ; speeds of stobjs when running M5 programs. ; [2] In an ACL2 session in which the symbol package "M5" has been ; defined, it may be selected as the ``current package'' by ; ACL2 >(in-package "M5") ; You will note that the ACL2 prompt will change to: ; M5 > ; to indicate that you are now typing symbols in the M5 package. ; One might criticize my choice to develop keytuples in the ; idiosyncratic package M5 instead of, say, the ACL2 package ; itself or some generic package. I chose to do it in M5 for two ; reasons. First, the symbols TYPE and GET are already defined in ; the ACL2 package and so I would have had to use different ; symbols for those functions or pick a new package name. Second, ; the fewer packages you have to contend with, the simpler your ; life is when experimenting with M5. ; ----------------------------------------------------------------- (in-package "M5") (defun type (keytuple) (car keytuple)) (defun has-keyp (key keytuple) (boundp key (cdr keytuple))) (defun get (key keytuple) (binding key (cdr keytuple))) (defun put (key val keytuple) (cons (type keytuple) (bind key val (cdr keytuple)))) (defthm put-put-3 (implies (and (not (equal key1 key2)) (has-keyp key2 x)) (equal (put key1 val1 (put key2 val2 x)) (put key2 val2 (put key1 val1 x)))) :rule-classes ((:rewrite :loop-stopper ((key1 key2))))) (defthm put-put-2 (implies (and (not (equal key1 key2)) (has-keyp key1 x)) (equal (put key1 val1 (put key2 val2 x)) (put key2 val2 (put key1 val1 x)))) :rule-classes ((:rewrite :loop-stopper ((key1 key2))))) (defthm put-put-1 (equal (put key val1 (put key val2 x)) (put key val1 x))) (defthm get-put (equal (get key1 (put key2 val2 x)) (if (equal key1 key2) val2 (get key1 x)))) (defun concat-atoms1 (lst) (if (endp lst) nil (append (explode-atom (car lst) 10) (concat-atoms1 (cdr lst))))) (defun concat-atoms (lst) (intern-in-package-of-symbol (coerce (concat-atoms1 lst) 'string) 'concat-atoms)) (defun unkeywordify (sym) (intern-in-package-of-symbol (symbol-name sym) 'unkeywordify)) (defun unkeywordify-lst (lst) (cond ((endp lst) nil) (t (cons (unkeywordify (car lst)) (unkeywordify-lst (cdr lst)))))) (defun make-keytuple-exprs (slotnames) (cond ((endp slotnames) nil) (t (cons `(list ,(car slotnames) ,(unkeywordify (car slotnames))) (make-keytuple-exprs (cdr slotnames)))))) (defun make-keytuple-accessor-macros (slotnames) (cond ((endp slotnames) nil) (t (cons `(defmacro ,(unkeywordify (car slotnames)) (x) `(get ,,(car slotnames) ,x)) (make-keytuple-accessor-macros (cdr slotnames)))))) ; The recursion here is a little silly but follows the same ; scheme used later, where it is quite elegant. (defun make-keytuple-has-keyp-rules (name done todo) (cond ((endp todo) nil) (t (cons `(equal (has-keyp ,(car todo) (,name ,@done ,@todo)) t) (make-keytuple-has-keyp-rules name (append done (list (car todo))) (cdr todo)))))) (defun make-keytuple-get-rules (name done todo) (cond ((endp todo) nil) (t (cons `(equal (get ,(car todo) (,name ,@(unkeywordify-lst done) ,@(unkeywordify-lst todo))) ,(unkeywordify (car todo))) (make-keytuple-get-rules name (append done (list (car todo))) (cdr todo)))))) (defun make-keytuple-put-rules (name done todo) (cond ((endp todo) nil) (t (cons `(equal (put ,(car todo) ,(concat-atoms (list 'new- (car todo) '-val)) (,name ,@(unkeywordify-lst done) ,@(unkeywordify-lst todo))) (,name ,@(unkeywordify-lst done) ,(concat-atoms (list 'new- (car todo) '-val)) ,@(unkeywordify-lst (cdr todo)))) (make-keytuple-put-rules name (append done (list (car todo))) (cdr todo)))))) ; The macros below use ACL2's :guard feature. We won't discuss the feature in ; this course. But it is a form of type checking. By including a guard in the ; definition of defkeytuple, for example, we insure that if you use defkeytuple ; in an ill-formed or illegal way, an error is signalled. (defmacro defkeytuple (name slotnames acl2::&key allow-but-ignore) (declare (xargs :guard (and (symbolp name) (symbol-listp slotnames) (symbol-listp allow-but-ignore) (no-duplicatesp (cons name (append slotnames allow-but-ignore)))))) `(encapsulate nil (set-inhibit-warnings "non-rec") (defun ,name ,(unkeywordify-lst slotnames) (declare (xargs :guard t)) (cons ',name (list ,@(make-keytuple-exprs slotnames)))) (defmacro ,(concat-atoms (list name '-macro)) (acl2::&key ,@(unkeywordify-lst slotnames) ,@(unkeywordify-lst allow-but-ignore)) ,@(if allow-but-ignore `((DECLARE (IGNORE ,@(unkeywordify-lst allow-but-ignore)))) nil) `(,',name . ,,(if slotnames (xxxjoin 'cons (append (unkeywordify-lst slotnames) '(nil))) nil))) ,@(make-keytuple-accessor-macros slotnames) (defthm ,(concat-atoms (list name '-rules)) (and (equal (type (,name ,@(unkeywordify-lst slotnames))) ',name) ,@(make-keytuple-has-keyp-rules name nil slotnames) ,@(make-keytuple-get-rules name nil slotnames) ,@(make-keytuple-put-rules name nil slotnames)) :hints (("Goal" :in-theory (enable type has-keyp get put)))) (in-theory (disable ,name)))) (defmacro make (name &rest args) (declare (xargs :guard (and (symbolp name) (keyword-value-listp args)))) `(,(concat-atoms (list name '-macro)) ,@args)) (in-theory (disable type has-keyp get put)) ; Now we deal with lists of keytuples. The following function searches through ; a list of keytuples for the first one that has the key value val. It returns ; that keytuple. We assume that all the keytuples in the list have the field ; named key. (defun find (key val lst) (if (endp lst) nil (if (equal (get key (car lst)) val) (car lst) (find key val (cdr lst))))) ; The following function searches a list for the first keytuple whose ; key is val and replaces that keytuple with the keytuple new. It ; returns the ``modified'' list. (defun replace (key val new lst) (if (endp lst) (list new) (if (equal (get key (car lst)) val) (cons new (cdr lst)) (cons (car lst) (replace key val new (cdr lst)))))) ; The following lemma tells us how to simplify a find-replace nest, if ; we're looking for a single key and the replace preserves the value ; of that key. (defthm find-replace (implies (equal (get key new) val2) (equal (find key val1 (replace key val2 new lst)) (if (equal val1 val2) new (find key val1 lst)))))#|ACL2s-ToDo-Line|#