;;; Tue Mar 9 21:18:00 1993 by Mark Kantrowitz ;;; infix.lisp -- 32284 bytes ;;; ************************************************************************** ;;; Infix ******************************************************************** ;;; ************************************************************************** ;;; ;;; This is an implementation of an infix reader macro. It should run in any ;;; valid Common Lisp and has been tested in Allegro CL 4.1, Lucid CL 4.0.1, ;;; MCL 2.0 and CMU CL. It allows the user to type arithmetic expressions in ;;; the traditional way (e.g., 1+2) when writing Lisp programs instead of ;;; using the normal Lisp syntax (e.g., (+ 1 2)). It is not intended to be a ;;; full replacement for the normal Lisp syntax. If you want a more complete ;;; alternate syntax for Lisp, get a copy Apple's MLisp or Pratt's CGOL. ;;; ;;; Although similar in concept to the Symbolics infix reader (#), ;;; no real effort has been made to ensure compatibility beyond coverage ;;; of at least the same set of basic arithmetic operators. There are several ;;; differences in the syntax beyond just the choice of #$ as the macro ;;; character. (Our syntax is a little bit more C-like than the Symbolics ;;; macro in addition to some more subtle differences.) ;;; We chose $ because of it's association with mathematics in LaTeX. ;;; (It's also one of the few characters that wasn't used as an operator.) ;;; ;;; Written by Mark Kantrowitz, School of Computer Science, ;;; Carnegie Mellon University, March 1993. ;;; ;;; Copyright (c) 1993 by Mark Kantrowitz. All rights reserved. ;;; ;;; Use and copying of this software and preparation of derivative works ;;; based upon this software are permitted, so long as the following ;;; conditions are met: ;;; o no fees or compensation are charged for use, copies, ;;; distribution or access to this software ;;; o this copyright notice is included intact. ;;; This software is made available AS IS, and no warranty is made about ;;; the software or its performance. ;;; ;;; In no event will the author(s) or their institutions be liable to you for ;;; damages, including lost profits, lost monies, or other special, incidental ;;; or consequential damages, arising out of or in connection with the use or ;;; inability to use (including but not limited to loss of data or data being ;;; rendered inaccurate or losses sustained by third parties or a failure of ;;; the program to operate as documented) the program, or for any claim by ;;; any other party, whether in an action of contract, negligence, or ;;; other tortious action. ;;; ;;; Please send bug reports, comments and suggestions to mkant@cs.cmu.edu. ;;; ;;; The current version of this software and a variety of related utilities ;;; may be obtained from the Lisp Utilities Repository by anonymous ftp ;;; from ftp.cs.cmu.edu [128.2.206.173] in the directory ;;; /afs/cs.cmu.edu/user/mkant/Public/Lisp/ ;;; You must cd to this directory in one atomic operation, as some of ;;; the superior directories on the path are protected from access by ;;; anonymous ftp. If your site runs the Andrew File System, you can just ;;; access the files directly without bothering with FTP. ;;; ;;; If you wish to be added to the Lisp-Utilities@cs.cmu.edu mailing list, ;;; send email to Lisp-Utilities-Request@cs.cmu.edu with your name, email ;;; address, and affiliation. This mailing list is primarily for ;;; notification about major updates, bug fixes, and additions to the Lisp ;;; Utilities Repository. The mailing list is intended to have low traffic. ;;; ;;; ******************************** ;;; Documentation ****************** ;;; ******************************** ;;; ;;; Syntax: ;;; ;;; Begin the reader macro with #$ and end it with $. For example, ;;; #$ x^^2 + y^^2 $ ;;; is equivalent to the Lisp form ;;; (+ (expt x 2) (expt y 2)) ;;; but much easier to read according to some folks. ;;; ;;; If you want to see the expansion, type a quote before the #$ form ;;; at the Lisp prompt: ;;; > '#$if x x-y ;;; ! lisp escape !(foo bar) --> (foo bar) ;;; ; comment ;;; x = y assignment (setf x y) ;;; x += y increment (incf x y) ;;; x -= y decrement (decf x y) ;;; x *= y multiply and store (setf x (* x y)) ;;; x /= y divide and store (setf x (/ x y)) ;;; x|y bitwise logical inclusive or (logior x y) ;;; x^y bitwise logical exclusive or (logxor x y) ;;; x&y bitwise logical and (logand x y) ;;; x<>y right shift (ash x (- y)) ;;; ~x ones complement (unary) (lognot x) ;;; x and y conjunction (and x y) ;;; x && y conjunction (and x y) ;;; x or y disjunction (or x y) ;;; x || y disjunction (or x y) ;;; not x negation (not x) ;;; x^^y exponentiation (expt x y) ;;; x,y sequence (progn x y) ;;; (x,y) sequence (progn x y) ;;; also parenthesis (x+y)/z --> (/ (+ x y) z) ;;; f(x,y) functions (f x y) ;;; a[i,j] array reference (aref a i j) ;;; x+y x*y arithmetic (+ x y) (* x y) ;;; x-y x/y arithmetic (- x y) (/ x y) ;;; -y value negation (- y) ;;; x % y remainder (mod x y) ;;; xy inequalities (< x y) (> x y) ;;; x <= y x >= y inequalities (<= x y) (>= x y) ;;; x == y equality (= x y) ;;; x != y equality (not (= x y)) ;;; if p then q conditional (when p q) ;;; if p then q else r conditional (if p q r) ;;; ;;; Precedence: ;;; ;;; The following precedence conventions are obeyed by the infix operators: ;;; [ ( ! ;;; ^^ ;;; ~ ;;; * / % ;;; + - ;;; << >> ;;; < == > <= != >= ;;; & ;;; ^ ;;; | ;;; not ;;; and ;;; or ;;; = += -= *= /= ;;; , ;;; if ;;; then else ;;; ] ) ;;; ;;; Note that logical negation has lower precedence than numeric comparison ;;; so that "not a> ) ( < == > <= != >= ) ( & ) ; logand ( ^ ) ; logxor ( \| ) ; logior ( not ) ( and ) ( or ) ;; Where should setf and friends go in the precedence? ( = += -= *= /= ) ( \, ) ; progn (statement delimiter) ( if ) ( then else ) ( \] \) ) ( %infix-end-token% )) ; end of infix expression "Ordered list of operators of equal precedence.") (defun operator-lessp (op1 op2) (dolist (ops *operator-ordering* nil) (cond ((find op1 ops :test #'same-token-p) (return nil)) ((find op2 ops :test #'same-token-p) (return t))))) (defparameter *right-associative-operators* '(^^ =)) (defun operator-right-associative-p (operator) (find operator *right-associative-operators*)) ;;; ******************************** ;;; Define Operators *************** ;;; ******************************** (defvar *token-operators* nil) (defvar *token-prefix-operator-table* (make-hash-table)) (defvar *token-infix-operator-table* (make-hash-table)) (defun token-operator-p (token) (find token *token-operators*)) (defun get-token-prefix-operator (token) (gethash token *token-prefix-operator-table*)) (defun get-token-infix-operator (token) (gethash token *token-infix-operator-table*)) (eval-when (compile load eval) (defmacro define-token-operator (operator-name &key (prefix nil prefix-p) (infix nil infix-p)) `(progn (pushnew ',operator-name *token-operators*) ,(when prefix-p `(setf (gethash ',operator-name *token-prefix-operator-table*) #'(lambda (stream) ,@(cond ((and (consp prefix) (eq (car prefix) 'infix-error)) ;; To avoid ugly compiler warnings. `((declare (ignore stream)) ,prefix)) (t (list prefix)))))) ,(when infix-p `(setf (gethash ',operator-name *token-infix-operator-table*) #'(lambda (stream left) ,@(cond ((and (consp infix) (eq (car infix) 'infix-error)) ;; To avoid ugly compiler warnings. `((declare (ignore stream left)) ,infix)) (t (list infix))))))))) ;;; Readtable definitions for characters, so that the right token is returned. (eval-when (compile load eval) (defmacro define-character-tokenization (char function) `(set-macro-character ,char ,function nil *infix-readtable*))) ;;; ******************************** ;;; Operator Definitions *********** ;;; ******************************** (define-token-operator and :infix `(and ,left ,(gather-superiors 'and stream))) (define-token-operator or :infix `(or ,left ,(gather-superiors 'or stream))) (define-token-operator not :prefix `(not ,(gather-superiors 'not stream))) (define-token-operator if :prefix (let* ((test (gather-superiors 'if stream)) (then (cond ((same-token-p (peek-token stream) 'then) (read-token stream) (gather-superiors 'then stream)) (t (infix-error "Missing THEN clause.")))) (else (when (same-token-p (peek-token stream) 'else) (read-token stream) (gather-superiors 'else stream)))) (cond ((and test then else) `(if ,test ,then ,else)) ((and test then) ;; no else clause `(when ,test ,then)) ((and test else) ;; no then clause `(unless ,test ,else)) (t ;; no then and else clauses --> always NIL nil)))) (define-token-operator then :prefix (infix-error "THEN clause without an IF.")) (define-token-operator else :prefix (infix-error "ELSE clause without an IF.")) (define-character-tokenization #\+ #'(lambda (stream char) (declare (ignore char)) (cond ((char= (peek-char nil stream t nil t) #\=) (read-char stream t nil t) '+=) (t '+)))) (define-token-operator + :infix `(+ ,left ,(gather-superiors '+ stream)) :prefix (gather-superiors '+ stream)) (define-token-operator += :infix `(incf ,left ,(gather-superiors '+= stream))) (define-character-tokenization #\- #'(lambda (stream char) (declare (ignore char)) (cond ((char= (peek-char nil stream t nil t) #\=) (read-char stream t nil t) '-=) (t '-)))) (define-token-operator - :infix `(- ,left ,(gather-superiors '- stream)) :prefix `(- ,(gather-superiors '- stream))) (define-token-operator -= :infix `(decf ,left ,(gather-superiors '-= stream))) (define-character-tokenization #\* #'(lambda (stream char) (declare (ignore char)) (cond ((char= (peek-char nil stream t nil t) #\=) (read-char stream t nil t) '*=) (t '*)))) (define-token-operator * :infix `(* ,left ,(gather-superiors '* stream))) (define-token-operator *= :infix `(,(if (symbolp left) 'setq 'setf) ,left (* ,left ,(gather-superiors '*= stream)))) (define-character-tokenization #\/ #'(lambda (stream char) (declare (ignore char)) (cond ((char= (peek-char nil stream t nil t) #\=) (read-char stream t nil t) '/=) (t '/)))) (define-token-operator / :infix `(/ ,left ,(gather-superiors '/ stream)) :prefix `(/ ,(gather-superiors '/ stream))) (define-token-operator /= :infix `(,(if (symbolp left) 'setq 'setf) ,left (/ ,left ,(gather-superiors '/= stream)))) (define-character-tokenization #\^ #'(lambda (stream char) (declare (ignore char)) (cond ((char= (peek-char nil stream t nil t) #\^) (read-char stream t nil t) '^^) (t '^)))) (define-token-operator ^^ :infix `(expt ,left ,(gather-superiors '^^ stream))) (define-token-operator ^ :infix `(logxor ,left ,(gather-superiors '^ stream))) (define-character-tokenization #\| #'(lambda (stream char) (declare (ignore char)) (cond ((char= (peek-char nil stream t nil t) #\|) (read-char stream t nil t) 'or) (t '\|)))) (define-token-operator \| :infix `(logior ,left ,(gather-superiors '\| stream))) (define-character-tokenization #\& #'(lambda (stream char) (declare (ignore char)) (cond ((char= (peek-char nil stream t nil t) #\&) (read-char stream t nil t) 'and) (t '\&)))) (define-token-operator \& :infix `(logand ,left ,(gather-superiors '\& stream))) (define-character-tokenization #\% #'(lambda (stream char) (declare (ignore stream char)) '\%)) (define-token-operator \% :infix `(mod ,left ,(gather-superiors '\% stream))) (define-character-tokenization #\~ #'(lambda (stream char) (declare (ignore stream char)) '\~)) (define-token-operator \~ :prefix `(lognot ,(gather-superiors '\~ stream))) (define-character-tokenization #\, #'(lambda (stream char) (declare (ignore stream char)) '\,)) (define-token-operator \, :infix `(progn ,left ,(gather-superiors '\, stream))) (define-character-tokenization #\= #'(lambda (stream char) (declare (ignore char)) (cond ((char= (peek-char nil stream t nil t) #\=) (read-char stream t nil t) '==) (t '=)))) (define-token-operator == :infix `(= ,left ,(gather-superiors '== stream))) (define-token-operator = :infix `(,(if (symbolp left) 'setq 'setf) ,left ,(gather-superiors '= stream))) (define-character-tokenization #\< #'(lambda (stream char) (declare (ignore char)) (cond ((char= (peek-char nil stream t nil t) #\=) (read-char stream t nil t) '<=) ((char= (peek-char nil stream t nil t) #\<) (read-char stream t nil t) '<<) (t '<)))) (define-token-operator < :infix `(< ,left ,(gather-superiors '< stream))) (define-token-operator <= :infix `(<= ,left ,(gather-superiors '<= stream))) (define-token-operator << :infix `(ash ,left ,(gather-superiors '<< stream))) (define-character-tokenization #\> #'(lambda (stream char) (declare (ignore char)) (cond ((char= (peek-char nil stream t nil t) #\=) (read-char stream t nil t) '>=) ((char= (peek-char nil stream t nil t) #\>) (read-char stream t nil t) '>>) (t '>)))) (define-token-operator > :infix `(> ,left ,(gather-superiors '> stream))) (define-token-operator >= :infix `(>= ,left ,(gather-superiors '>= stream))) (define-token-operator >> :infix `(ash ,left (- ,(gather-superiors '>> stream)))) (define-character-tokenization #\! #'(lambda (stream char) (declare (ignore char)) (cond ((char= (peek-char nil stream t nil t) #\=) (read-char stream t nil t) '!=) (t '!)))) (define-token-operator != :infix `(not (= ,left ,(gather-superiors '!= stream)))) (define-token-operator ! :prefix (read-regular stream)) (define-character-tokenization #\[ #'(lambda (stream char) (declare (ignore stream char)) '\[)) (define-token-operator \[ :infix (let ((indices (infix-read-delimited-list '\] '\, stream))) (if (null indices) (infix-error "No indices found in array reference.") `(aref ,left ,@indices)))) (define-character-tokenization #\( #'(lambda (stream char) (declare (ignore stream char)) '\()) (define-token-operator \( :infix `(,left ,@(infix-read-delimited-list '\) '\, stream)) :prefix (let ((list (infix-read-delimited-list '\) '\, stream))) (if (null (rest list)) ;; only one element in list. works correctly if list is NIL (first list) ;; several elements in list `(progn ,@list)))) (define-character-tokenization #\] #'(lambda (stream char) (declare (ignore stream char)) '\])) (define-token-operator \] :infix (infix-error "Extra close brace \"]\" in infix expression")) (define-character-tokenization #\) #'(lambda (stream char) (declare (ignore stream char)) '\))) (define-token-operator \) :infix (infix-error "Extra close paren \")\" in infix expression")) (define-character-tokenization #\$ #'(lambda (stream char) (declare (ignore stream char)) '%infix-end-token%)) (define-token-operator %infix-end-token% :infix (infix-error "Prematurely terminated infix expression") :prefix (infix-error "Prematurely terminated infix expression")) (define-character-tokenization #\; #'(lambda (stream char) (declare (ignore char)) (do ((char (peek-char nil stream t nil t) (peek-char nil stream t nil t))) ((or (char= char #\newline) (char= char #\return) (char= char #\$)) ;; Gobble characters until the end of the line or the ;; end of the input. (cond ((or (char= char #\newline) (char= char #\return)) (read-char stream) (read stream t nil t)) (t ;; i.e., return %infix-end-token% (read stream t nil t)))) (read-char stream)))) ;;; ******************************** ;;; Syntactic Modifications ******** ;;; ******************************** ;;; Post processes the expression to remove some unsightliness caused ;;; by the way infix processes the input. Note that it is also required ;;; for correctness in the a <= >= progn) :test #'same-operator-p)) ;; Flatten the expression if possible (cond ((and (eq operator '-) (= (length left) 2)) ;; -a-b --> (+ (- a) (- b)). `(+ ,left (- ,right))) ((and (eq operator '/) (= (length left) 2)) ;; ditto with / `(/ (* ,(second left) ,right))) (t ;; merges a+b+c as (+ a b c). (append left (list right))))) ((and (consp left) (eq operator '-) (eq (first left) '+)) ;; merges a+b-c as (+ a b (- c)). (append left (list `(- ,right)))) ((and (consp left) (find operator '(< > <= >=)) (find (first left) '(< > <= >=))) ;; a a>b" (ash a (- b))) ("~a" (lognot a)) ("a&&b" (and a b)) ("a||b" (or a b)) ("a%b" (mod a b)) ;; Comment character ("x^^2 ; the x coordinate + y^^2 ; the y coordinate" (+ (expt x 2) (expt y 2))) ;; Errors ("foo(bar,baz" :error) ; premature termination ("foo(bar,baz))" :error) ; extra close parenthesis ("foo[bar,baz]]" :error) ; extra close bracket ("[foo,bar]" :error) ; AREF is not a prefix operator ("and a" :error) ; AND is not a prefix operator ("< a" :error) ; < is not a prefix operator ("=bar" :error) ; SETF is not a prefix operator ("*bar" :error) ; * is not a prefix operator ("a not b" :error) ; NOT is not an infix operator ("a if b then c" :error) ; IF is not an infix operator ("" :error) ; premature termination (empty clause) (")a" :error) ; left parent is not a prefix operator ("]a" :error) ; left bracket is not a prefix operator )) (defun test-infix (&optional (tests *test-cases*)) (let ((count 0)) (dolist (test tests) (destructuring-bind (string result) test (unless (test-infix-case string result) (incf count)))) (format t "~&~:(~R~) test~p failed." count count) (values))) (defun test-infix-case (string result) (multiple-value-bind (value error) (let ((*package* (find-package "INFIX"))) (ignore-errors (values (read-from-string (concatenate 'string "#$" string "$") t nil)))) (cond (error (cond ((eq result :error) t) (t (format t "~&Test #$~A$ failed with ERROR." string) nil))) ((eq result :error) (format t "~&Test #$~A$ failed. ~ ~& Expected ERROR ~ ~& but got ~A." string value) nil) ((not (equal value result)) (format t "~&Test #$~A$ failed. ~ ~& Expected ~A ~ ~& but got ~A." string result value) nil) (t t)))) ;;; *EOF*