#| Copyright © 2022 by Pete Manolios CS 4820 Fall 2022 Homework 5. Due: 11/2 (Midnight) For this assignment, work in groups of 2-3. Send me exactly one solution per team and make sure to follow the submission instructions on the course Web page. In particular, make sure that the subject of your email submission is "CS 4820 HWK 4". The group members are: ... (put the names of the group members here) To make sure that we are all on the same page, build the latest version of ACL2s, as per HWK1. You are going to be using SBCL, which you already have, due to the build process in Next, install quicklisp. See https://www.quicklisp.org/beta/. To make sure everything is OK with quicklisp and to initialize it, start sbcl and issue the following commands (load "~/quicklisp/setup.lisp") (ql:quickload :trivia) (ql:quickload :cl-ppcre) (ql:quickload :let-plus) (setf ppcre:*allow-named-registers* t) (exit) Next, clone the ACL2s interface repository: (https) https://gitlab.com/acl2s/external-tool-support/interface.git (ssh) git@gitlab.com:acl2s/external-tool-support/interface.git This repository includes scripts for interfacing with ACL2s from lisp. Put this directory in the ...books/acl2s/ of your ACL2 repository, or use a symbolic link. Now, certify the books, by going to ...books/acl2s/interface and typing "cert.pl -j 4 top" Look at the documentation for cert.pl. If cert.pl isn't in your path, then use "...books/build/cert.pl -j 4 top" The "-j 4" option indicates that you want to run up to 4 instances of ACL2 in parallel. Set this number to the number of cores you have. As mentioned at the beginning of the semester, some of the code we will write is in Lisp. You can find the common lisp manual online in various formats by searching for "common lisp manual." Other references that you might find useful and are available online include - Common Lisp: A Gentle Introduction to Symbolic Computation by David Touretzky - ANSI Common Lisp by Paul Graham |# (in-package "ACL2S") #| Here is the solution to HWK3, Q3. |# (defdata if-atom (or bool var)) (defdata if-expr (or if-atom (list 'if if-expr if-expr if-expr))) (defdata norm-if-expr (or if-atom (list 'if if-atom norm-if-expr norm-if-expr))) ; Notice that norm-if-expr is a subtype of if-expr. (defdata-subtype-strict norm-if-expr if-expr) ; Here is the measure function we derived in class. (definec m-if-flat (x :if-expr) :pos (match x (:if-atom 1) (('if a b c) (b* ((ma (m-if-flat a)) (mb (m-if-flat b)) (mc (m-if-flat c))) (+ ma (* ma (+ mb mc))))))) ; Here is how we admit if-flat. See the documentation on ccg, ccg-xargs (definec if-flat (x :if-expr) :norm-if-expr (declare (xargs :consider-only-ccms ((m-if-flat x)))) (match x (:if-atom x) (('if a b c) (match a (:if-atom `(if ,a ,(if-flat b) ,(if-flat c))) (('if d e f) (if-flat `(if ,d (if ,e ,b ,c) (if ,f ,b ,c)))))))) #| We will now prove that if-flat does not change the semantics of if expressions. |# (defdata if-assign (alistof var bool)) ; Notice that if var is not in the if-assign, we return nil. (definec lookup-var (var :var a :if-assign) :bool (match a (nil nil) (((!var . val) . &) val) (& (lookup-var var (cdr a))))) (definec lookup-atom (e :if-atom a :if-assign) :bool (match e (:bool e) (& (lookup-var e a)))) (definec if-eval (e :if-expr a :if-assign) :bool (match e (:if-atom (lookup-atom e a)) (('if x y z) (if (if-eval x a) (if-eval y a) (if-eval z a))))) ; We prove that for all if-assign's, an if-expr e evaluates to ; the same thing as (if-flat e). (property if-expr-test-type (x :all y :all) (=> (^ (not (consp x)) (not (varp x)) (not (booleanp x))) (not (if-exprp (list* 'if x y)))) :hints (("goal" :expand (if-exprp (list* 'if x y))))) (property if-flat-equal-if (e :if-expr a :if-assign) (== (if-eval (if-flat e) a) (if-eval e a))) #| Check-validp is a simple validity checker for if-expr's. The idea is to use if-flat to normalize the if-expr. Then, we start with an empty if-assign and check validity by traversing the expression. When we get to a variable that is not assigned, we check that the expression is a validity when the variable is t and when it is nil. |# ; Lookup assoc-equal in the documentation. (definec assignedp (e :if-atom a :if-assign) :bool (or (boolp e) (consp (assoc-equal e a)))) (definec validp (e :norm-if-expr a :if-assign) :bool (match e (:if-atom (lookup-atom e a)) (('if x y z) (if (assignedp x a) (if (lookup-atom x a) (validp y a) (validp z a)) (and (validp y (acons x t a)) (validp z (acons x nil a))))))) (definec check-validp (e :if-expr) :bool (validp (if-flat e) nil)) (property if-eval-ignores-redundant-assignments (e :if-expr v :var val :bool a :if-assign) (=> (== (lookup-var v a) val) (== (if-eval e (cons (cons v val) a)) (if-eval e a)))) (property lookup-app (v :var a :if-assign b :if-assign) (== (lookup-var v (append a b)) (if (assoc-equal v a) (lookup-var v a) (lookup-var v b)))) (property lookup-assoc (a :if-assign v :var) (=> (lookup-var v a) (assoc-equal v a))) (property validp-implies-if-eval (e :norm-if-expr a :if-assign b :if-assign) (=> (validp e b) (if-eval e (append b a))) :rule-classes nil) (property check-validp-is-sound (e :if-expr a :if-assign) (=> (check-validp e) (if-eval e a)) :hints (("Goal" :use (:instance validp-implies-if-eval (e (if-flat e)) (b nil))))) #| Question 1. (25 pts) Prove that check-validp is complete by showing that when check-validp returns nil, there exists an if-assign under which the if-expr evaluates to nil. With this proof, we now know that check-validp is a decision procedure for PL validity. Suggested approach: (1) Define a function, falsify, that given an if-expr will return a falsifying assignment, if one exists. (2) Prove that if (check-validp e) returns nil, then the assignment returned by (falsify e) is a falsifying assignment, i.e., (if-eval e (falisfy e)) is nil. (3) You must state the main theorem using existential quantification ala defun-sk. See the readings on quantification in ACL2. (This part is easy once you do 1, 2 above.) For defun-sk you should add guards (see the documentation) since you don't have the definec syntax to specify types. You'll need a declaration like this: (declare (xargs :guard (if-exprp e))) Or, you can use :check-contracts? nil in the property you define. Remember the lecture on using ACL2s effectively and think first! |# ... ;; Now for some ACL2s systems programming. ;; This book is already included in ACL2s, so this is a no-op, but I'm ;; putting it here so that you can see where the code for ACL2s ;; systems programming is coming from. (include-book "acl2s/interface/top" :dir :system) (set-ignore-ok t) ;; This gets us to raw lisp. :q #| The interface books provide us with the ability to call the theorem prover within lisp, which will be useful in checking your code. Here are some examples you can try. acl2s-compute is the form you use when you are using ACL2s to compute something, e.g., running a function on some input. acl2s-query is the form you use when you are querying ACL2s, e.g., a property without a name. If the property has a name, then that is not a query, but an event and you have to use acl2s-event. (acl2s-compute '(+ 1 2)) (acl2s-query '(property (p :all q :all) (iff (=> p q) (v (! p) q)))) |# #| Next, we need to load some software libraries using quicklisp. For example, the trivia library provides pattern matching capabilities. Search for "match" below. Links to online documentation for the software libraries are provided below. |# (load "~/quicklisp/setup.lisp") ; See https://lispcookbook.github.io/cl-cookbook/pattern_matching.html (ql:quickload :trivia) ; See https://www.quicklisp.org/beta/UNOFFICIAL/docs/cl-ppcre/doc/index.html (ql:quickload :cl-ppcre) ; See https://github.com/sharplispers/let-plus (ql:quickload :let-plus) (setf ppcre:*allow-named-registers* t) #| We now define our own package. |# (defpackage :tp (:use :cl :trivia :ppcre :let-plus :acl2 :acl2s)) (in-package :tp) ;; We import acl2s-compute and acl2s-query into our package. (import 'acl2s-interface-internal::(acl2s-compute acl2s-query)) #| We have a list of the propositional operators and information about them. :arity can be a positive integer or - (meaning arbitrary arity) If :arity is -, there must be an identity and the function must be associative and commutative. If :identity is non-nil, then the operator has the indicated identity. An operator is idempotent iff :idem is t. If :sink is not -, then it must be the case that (op ... sink ...) = sink, e.g., (and ... nil ...) = nil. FYI: it is common for global variables to be enclosed in *'s. |# (defparameter *p-ops* '((and :arity - :identity t :idem t :sink nil) (or :arity - :identity nil :idem t :sink t) (not :arity 1 :identity - :idem nil :sink -) (implies :arity 2 :identity - :idem nil :sink -) (iff :arity - :identity t :idem nil :sink -) (xor :arity - :identity nil :idem nil :sink -) (if :arity 3 :identity - :idem nil :sink -))) #| mapcar is like map. See the common lisp manual. In general if you have questions about lisp, ask on Piazza. |# (defparameter *p-funs* (mapcar #'car *p-ops*)) #| See the definition of member in the common lisp manual. Notice that there are different types of equality, including =, eql, eq, equal and equals. We need to be careful, so we'll just use equal and we'll define functions that are similar to the ACL2s functions we're familiar with. |# (defun in (a x) (member a x :test #'equal)) (defmacro len (l) `(length ,l)) (defun p-funp (x) (in x *p-funs*)) (defun key-alist->val (k l) (let* ((in (assoc k l :test #'equal))) (values (cdr in) in))) (key-alist->val 'iff *p-ops*) (defun key-list->val (k l) (let* ((in (member k l :test #'equal))) (values (cadr in) in))) (key-list->val ':arity (key-alist->val 'iff *p-ops*)) (defun pfun-key->val (fun key) (key-list->val key (key-alist->val fun *p-ops*))) (defun remove-dups (l) (remove-duplicates l :test #'equal)) (defmacro == (x y) `(equal ,x ,y)) (defmacro != (x y) `(not (equal ,x ,y))) (defparameter *booleans* '(t nil)) (defun booleanp (x) (in x *booleans*)) (defun pfun-argsp (pop args) (and (p-funp pop) (let ((arity (key-list->val :arity (key-alist->val pop *p-ops*)))) (and (or (== arity '-) (== (len args) arity)) (every #'p-formulap args))))) #| Here is the definition of a propositional formula. |# (defun p-formulap (f) (match f ((type boolean) t) ; don't need this case, but here for emphasis ((type symbol) t) ((list* pop args) (if (p-funp pop) (pfun-argsp pop args) t)) (_ nil))) #| Notice that in addition to propositional variables, we allow atoms such as (foo x). Here are some assertions (see the common lisp manual). |# (assert (p-formulap '(and))) (assert (p-formulap '(and x y z))) (assert (p-formulap '(and t nil))) (assert (not (p-formulap '(implies x t nil)))) (assert (p-formulap 'q)) (assert (p-formulap '(implies (foo x) (bar y)))) (assert (p-formulap '(iff p q r s t))) (assert (p-formulap '(xor p q r s t))) (assert (not (p-formulap '(if p q r t)))) #| The propositional skeleton is what remains when we remove non-variable atoms. |# (defun p-skeleton-args (args amap acc) (if (endp args) (values (reverse acc) amap) (let+ (((&values na namap) (p-skeleton (car args) amap))) (p-skeleton-args (cdr args) namap (cons na acc))))) (defun p-skeleton (f &optional amap) ;amap is nil if not specified (match f ((type symbol) (values f amap)) ((list* pop args) (if (p-funp pop) (let+ (((&values nargs namap) (p-skeleton-args args amap nil))) (values (cons pop nargs) namap)) (let ((geta (key-alist->val f amap))) (if geta (values geta amap) (let ((gen (gentemp "P"))) (values gen (acons f gen amap))))))) (_ (error "Not a p-formula")))) #| Here are some examples you can try. (p-skeleton '(or p q r s)) (p-skeleton '(iff q r)) (p-skeleton '(or p (iff q r))) (p-skeleton '(or p (iff q r) (and p q p) (if p (and p q) (or p q)))) (p-skeleton '(iff p p q (foo t nil) (foo t nil) (or p q))) (p-skeleton '(xor p p q (foo t nil) (foo t nil) (or p q))) (p-skeleton '(iff p p q (foo t r) (foo s nil) (or p q))) (p-skeleton '(or (foo a (g a c)) (g a c) (not (foo a (g a c))))) |# #| Next we have some utilities for converting propositional formulas to ACL2s formulas. |# (defun nary-to-2ary (fun args) (let ((identity (pfun-key->val fun :identity))) (match args (nil identity) ((list x) (to-acl2s x)) (_ (acl2s::xxxjoin (to-acl2s fun) (mapcar #'to-acl2s args)))))) (defun to-acl2s (f) (let ((s (p-skeleton f))) (match s ((type symbol) (intern (symbol-name f) "ACL2S")) ((cons x xs) (if (in x '(iff xor)) (nary-to-2ary x xs) (mapcar #'to-acl2s f))) (_ f)))) (to-acl2s '(and a b c d)) (to-acl2s '(iff a b c d)) (to-acl2s '(xor a b c d)) (defun pvars- (f) (match f ((type boolean) nil) ((type symbol) (list f)) ((list* op args) (and (p-funp op) (reduce #'append (mapcar #'pvars- args)))))) (defun pvars (f) (remove-dups (pvars- f))) (pvars '(and t (iff nil) (iff t nil t nil) p q)) (pvars '(iff p p q (foo t r) (foo s nil) (or p q))) (pvars '(if p q (or r (foo t s) (and (not q))))) (defun boolean-hyps (l) (reduce #'append (mapcar #'(lambda (x) `(,x :bool)) (mapcar #'to-acl2s l)))) (boolean-hyps '(p q r)) (defun acl2s-check-equal (f g) (let* ((iff `(iff ,f ,g)) (siff (p-skeleton iff)) (pvars (pvars siff)) (aiff (to-acl2s siff)) (af (second aiff)) (ag (third aiff)) (bhyps (boolean-hyps pvars))) (acl2s-query `(acl2s::property ,bhyps (acl2s::iff ,af ,ag))))) ;; And some simple examples. (acl2s-check-equal 'p 'p) (acl2s-check-equal 'p 'q) ; Here is how to check if the query succeeded (assert (== (car (acl2s-check-equal 'p 'p)) nil)) ; So, here's a useful function (defun assert-acl2s-equal (f g) (assert (== (car (acl2s-check-equal f g)) nil))) (assert-acl2s-equal 'p 'p) #| ; This will lead to an error. Try it. ; In sbcl :top gets you out of the debugger. (assert-acl2s-equal 'p 'q) |# ; Here is how we can use ACL2s to check our code. (let* ((x '(or (foo a (g a c)) (g a c) (not (foo a (g a c)))))) (assert-acl2s-equal x t)) (let* ((x '(or (foo a (g a c)) (g a c) (not (foo a (g a c))))) (sx (p-skeleton x))) (assert-acl2s-equal sx t)) #| Question 2. (25 pts) Define function, p-simplify that given a propositional formula returns an equivalent propositional formula with the following properties. A. The skeleton of the returned formula is either a constant or does not include any constants. For example: (and p t (foo t nil) q) should be simplified to (and p (foo t nil) q) (and p t (foo t b) nil) should be simplified to nil B. Flatten expressions, e.g.: (and p q (and r s) (or u v)) is not flat, but this is (and p q r s (or u v)) A formula of the form (op ...) where op is a Boolean operator of arbitrary arity (ie, and, or, iff) applied to 0 or 1 arguments is not flat. For example, replace (and) with t. A formula of the form (op ... (op ...)) where op is a Boolean operator of arbitrary arity is not flat. For example, replace (and p q (and r s)) with (and p q r s). C. If there is Boolean constant s s.t. If (op ... s ...) = s, then we say that s is a sink of op. For example t is a sink of or. A formula is sink-free if no such subformulas remain. The returned formula should be sink-free. D. Simplify your formulas so that no subexpressions of the following form remain (not (not f)) (not (iff ...)) (not (xor ...)) E. Apply Shannon expansions in 61-67. For example (and (or p q) (or r q p) p) should be simplified to p because (and (or t q) (or r q t) p) is (and t t p) is p F. Simplify formulas so that no subexpressions of the form (op ... p ... q ...) where p, q are equal literals or p = (not q) or q = (not p). For example (or x y (foo a b) z (not (foo a b)) w) should be simplified to t Make sure that your algorithm is as efficient as you can make it. The idea is to use this simplification as a preprocessing step, so it needs to be fast. You are not required to perform any other simplifications beyond those specified above. If you do, your simplifier must be guaranteed to always return something that is simpler that what would be returned if you just implemented the simplifications explicitly requested. Also, if you implement any other simplifications, your algorithm must run in comparable time (eg, no validity checking). Notice some simple consequences. You cannot transform the formula to an equivalent formula that uses a small subset of the connectives (such as not/and). If you do that, the formula you get can be exponentially larger than the input formula, as we have discussed in class. Notice that even negation normal form (NNF) can increase the size of a formula. Such considerations are important when you use Tseitin, because experience has shown that even transformations such as NNF are usually a bad idea when generating CNF, as they tend to result in CNF formulas that take modern solvers longer to analyze. Test your definition with assert-acl2s-equal using at least 10 propositional formulas that include non-variable atoms, all of the operators, deeply nested formulas, etc. |# (defun p-simplify (f) ...) #| Question 3. (20 pts) Define tseitin, a function that given a propositional formula, something that satisfies p-formulap, applies the tseitin transformation to generate a CNF formula that is equi-satisfiable. Remember that you have to deal with atoms such as (foo (if a b)) You should simplify the formula first, using p-simplify, but do not perform any other simplifications. Any simpification you want to perform must be done in p-simplify. Test tseitin using with assert-acl2s-equal using at least 10 propositional formulas. |# (defun tseitin (f) ...) #| Question 4. (30 pts) Define DP, a function that given a propositional formula in CNF, applies the Davis-Putnam algorithm to determine if the formula is satisfiable. Remember that you have to deal with atoms such as (foo (if a b)) If the formula is sat, DP returns 'sat and a satisfying assignment: an alist mapping each atom in the formula to t/nil. Use values to return multiple values. If it is usat, return 'unsat. Do some profiling Test DP using with assert-acl2s-equal using at least 10 propositional formulas. It is easy to extend dp to support arbitrary formulas by using tseitin to generate CNF. |# (defun dp (f) ...) #| Question 5. Extra Credit Part1: (20pts) Profile DP and make it as efficient as possible. If it meets the efficiency criteria of the evaluator, you get credit. The fastest submission will get credit, no matter how slow. To generate interesting problems, see your book. Part 2: (30pts) Define DPLL, a function that given a propositional formula in CNF, applies the DPLL algorithm to determine if the formula is satisfiable. You have to implement the iterative with backjumping version of DPLL from the book. Remember that you have to deal with atoms such as (foo (if a b)) If the formula is sat, DPLL returns 'sat and a satisfying assignment: an alist mapping each atom in the formula to t/nil. Use values to return multiple values. If it is usat, return 'unsat. Test DPLL using with assert-acl2s-equal using at least 10 propositional formulas. (defun dpll (f) ...) |#