123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- ;; eval.lisp -- Evaluate parsed proposition strings
- ;; Copyright (C) 2024 Alexander Rosenberg
- ;;
- ;; This program is free software: you can redistribute it and/or modify
- ;; it under the terms of the GNU General Public License as published by
- ;; the Free Software Foundation, either version 3 of the License, or
- ;; (at your option) any later version.
- ;;
- ;; This program is distributed in the hope that it will be useful,
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;; GNU General Public License for more details.
- ;;
- ;; You should have received a copy of the GNU General Public License
- ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
- (in-package :truth-table/base)
- (define-condition proposition-eval-error (error)
- ((message :initarg :message
- :accessor proposition-eval-error-message)
- (proposition :initarg :proposition
- :accessor proposition-eval-error-proposition
- :initform nil))
- (:report (lambda (con stream)
- (with-slots (message proposition)
- con
- (format stream "~a~@[:~% ~a~]"
- message proposition))))
- (:documentation "Condition representing an error that occurred during
- evaluation for a proposition."))
- (defun operator-argument-count (oper)
- "Return the minimum number of arguments that OPER takes as the first value,
- and the maximum number (or nil for infinity) as a second value."
- (case oper
- (and (values 1 nil))
- (or (values 1 nil))
- (xor (values 1 nil))
- (not (values 1 1))
- (implies (values 2 2))
- (converse (values 2 2))
- (iff (values 2 2))
- (nand (values 1 nil))
- (nor (values 1 nil))
- (t (error "unknown operator: ~S" oper))))
- (defun logical-xor (&rest args)
- "Logical xor (not equal) each argument in turn with its following argument.
- NOTE: This is NOT a macro, there is no short circuit evaluation (all arguments
- are evaluated no matter what)."
- (loop with result = nil
- for arg in args do
- (setq result (not (eq result arg)))
- finally (return result)))
- (defun logical-and (&rest args)
- "Logical and (all true).
- NOTE: This is NOT a macro, there is no short circuit evaluation (all arguments
- are evaluated no matter what)."
- (not (member nil args)))
- (defun logical-or (&rest args)
- "Logical or (one or more true).
- NOTE: This is NOT a macro, so there is no short circuit evaluation (all
- arguments are evaluated no matter what)."
- (not (not (member t args))))
- (defun logical-implies (prop1 prop2)
- "Evaluate the logical implies operation on PROP1 and PROP2. That is \"if
- PROP1, then PROP2\".
- NOTE: This is NOT a macro, so there is no short circuit evaluation (all
- arguments are evaluated no matter what)."
- (if prop1 ;; only if first is true
- prop2 ;; eval second
- t)) ;; otherwise, just return true
- (defun check-operator-argument-count (oper args)
- "Raise an error if OPER cannot be called with ARGS."
- (multiple-value-bind (min max) (operator-argument-count oper)
- (let ((arg-count (length args)))
- (cond
- ((< arg-count min)
- (error 'proposition-eval-error
- :message
- (format nil "~s ~[takes no arguments~;requires one argument~:;~
- requires at least ~:*~d arguments~], ~
- but got ~[none~:;~:*~d~]"
- oper min arg-count)))
- ((and max (> arg-count max))
- (error 'proposition-eval-error
- :message
- (format nil "~s can take at most ~d argument~:p, but got ~d"
- oper max arg-count)))))))
- (defun keep-unique-expressions (mapping)
- "Keep only unique expressions from MAPPING, which is an alist as returned by
- `eval-proposition'."
- (loop for entry in mapping
- unless (assoc (car entry) output :test 'equal)
- collect entry into output
- finally (return output)))
- (defun eval-proposition (prop vars)
- "Evaluate the proposition PROP, with the alist VARS mapping variables to their
- values. Return the result of the proposition as the first value, and an alist
- mapping sub expressions to their results as the second value.
- NOTE: the second value does not include individual variables, literal values
- (true and false)."
- (cond
- ;; prop is a variable name
- ((stringp prop)
- (let ((entry (assoc prop vars :test 'equal)))
- (unless entry
- (error 'proposition-eval-error
- :message (format nil "unknown variable: ~S~%" prop)))
- (values (cdr entry) '())))
- ;; prop is true or false
- ((eq prop 'true)
- (values t '()))
- ((eq prop 'false)
- (values nil '()))
- ;; prop is a compound expression
- (t
- (loop with (oper . args) = prop
- for arg in args
- for (value sub-map) = (multiple-value-list
- (eval-proposition arg vars))
- nconc sub-map into mapping
- collect value into arg-values
- finally
- (check-operator-argument-count oper args)
- (let ((result
- (case oper
- ;; avoid using the macros `and' and `or' so we can avoid
- ;; using `eval'
- (and
- (apply 'logical-and arg-values))
- (nand
- (not (apply 'logical-and arg-values)))
- (or
- (apply 'logical-or arg-values))
- (nor
- (not (apply 'logical-or arg-values)))
- (xor
- (apply 'logical-xor arg-values))
- (not
- (not (car arg-values)))
- (implies
- (logical-implies (car arg-values)
- (second arg-values)))
- (converse
- ;; this is just implies with the arguments flipped
- (logical-implies (second arg-values)
- (car arg-values)))
- (iff
- (eq (car arg-values) ;; both must have the same value
- (second arg-values))))))
- (return (values result
- (keep-unique-expressions
- (cons (cons prop result) mapping)))))))))
|