Common Lisp

RPN Calculator in Common Lisp

The main procedure, rpn, reads an input line into the string s and passes it to eval-rpn for evaluating the RPN expression. For ease of further processing, tab characters in s are replaced by spaces.

Within eval-rpn, expression evaluation progresses through extracting tokens one at a time and analyzing each one. The input string is correspondingly shortened after each extraction. A stack of intermediate values st is maintained along the process. For obtaining and working on each new token eval-rpn calls itself again with updated input string and stack.

St is an optional argument to eval-rpn defaulting to an empty list. The default value is only used as an initial one in the call from rpn.

If a syntax error is found in the RPN expression, a signal is issued with the predefined condition name error as an argument. Nesting the call of eval-rpn within a call of handler-case results in trapping the signal by the (only) error clause of handler-case, which to this end is labeled the same way as the signal: error. Handling the signal consists of printing an error message.

If an RPN expression is evaluated successfully, the keyword-labeled clause :no-error of handler-case receives control, binding the name r to the result. If not nil, r is printed out using write and terpri (the latter being for terminate print line). Float is applied on r before actually printing it, because r may happen to be a rational number, and we want to print all results as floats; if float is omitted, only those results that really are not rational will print in decimal-dot notation. (Lisp guarantees an exact rational result for any arithmetic computation, provided that none of the participating numbers is a floating-point one.)

Note that r indeed can be nil, and this happens if and only if the input line contained no tokens at all.

Whatever the outcome of the evaluation, rpn proceeds to call itself again so that reading and evaluation are repeated.

Read-line reports end-of-input to rpn by returning the value nil instead of an actual string. That value is detected by means of the null predicate, so that if (null s) holds, rpn does nothing but return.

Obtaining a token within eval-rpn begins by stripping the string s off any possible initial blank characters. Thus, if s then becomes empty, there are no (more) tokens to be processed and the procedure concludes by popping the result of evaluation off the stack and returning it. The stack, however, can be empty – if the input line was initially empty or all-blanks – and then the procedure returns nil, since that is the value returned by pop for an empty stack. More than one item present in the stack is an indication of an RPN syntax error, which is properly signaled.

For a non-empty s, in i we find the position one after the first token in s (which is either the first blank from the beginning of the string, or the index immediately past the end of s if there are no blanks). The token is stored in tk and subjected to qualification tests.

(The use of or here is a typical example of how a seemingly boolean operation is in fact a control construct: or evaluates its arguments in sequence and stops as soon as it finds one with a non-nil value, then it returns that value. It will only return nil when all its arguments are nil. Similarly, and evaluates until an argument is found that is nil, and it only returns a non-nil when that is the value of its last argument, and none of the preceding ones is nil. Scheme is very similar in this respect.)

Discrimination between the cases of an arithmetic operator, a number, and anything else for tk takes place within a cond (conditional) form. In the first clause of cond, besides checking whether tk represents an operator, by calling second it is made sure that the stack contains (at least) two numbers necessary to apply the operation on. Applying itself is done by calling funcall on the values y and x popped off the stack. Note that read-from-string is used to obtain the respective atom from the string tk. This is needed because that atom is the only means to refer to the function performing the operation whose name is tk.

The result of applying an arithmetic operator tk, or the number represented by tk itself, eventually becomes the result of cond and so gets pushed onto the stack. If tk is neither of these, the third clause of cond is reached, and as it is guarded by a constantly true expression (t), an error signal is issued.

The get-number procedure finds out whether a token parses as a numeric value, returning that value on success and nil if parsing fails. It does that by testing the kinds and the number of characters of each kind in the token. More precisely, get-number ensures that there is at least one digit, at most one decimal dot, a possible leading arithmetic sign, and no other characters in the string. Obtaining the number from an already proven correct string is done by calling read-from-string, as described above for the kind of atom representing an operator.

Read-from-string is a representative of the so called reader mechanism of Lisp, which is indeed a lexer: it is used for lexical analysis on strings, including mapping the recognized tokens to (Lisp) data objects. We could have implemented the calculator using as a tokenizer another, more general Lisp procedure called read. Successive calls of read applied on an input stream which appears to be a string will extract tokens from that stream until the latter is exhausted. Potentially, this could have saved us the labour of extracting individual tokens and finding out what data object each one represents. In practice, however, this does not work easily. As read is a Lisp-oriented tokenizer, it will try to recognize tokens as defined in the Lisp language, entirely skipping comments and admitting a wide range of number representations: all these must be treated as errors in the calculator. Therefore some additional work is needed in order to warrant against such errors, eventually canceling out the benefits of using read.

(defun get-number (s)
  (let* ((i (if (find (elt s 0) "+-") 1 0))
         (d (count-if 'digit-char-p s :start i))
         (f (count #\. s :start i)))
    (and (> d 0) (< f 2) (= (+ i f d) (length s)) (read-from-string s))))

(defun eval-rpn (s &optional (st '()))
  (let ((s (string-left-trim " " s)))
    (if (string/= s "")
      (let* ((i (or (position #\Space s) (length s))) (tk (subseq s 0 i)))
        (eval-rpn (subseq s i)
          (push (cond ((and (= 1 (length tk)) (find (elt tk 0) "+-*/")
                            (second st))
                        (let* ((y (pop st)) (x (pop st)))
                          (funcall (read-from-string tk) x y)))
                      ((get-number tk))
                      (t (signal 'error))) st)))
      (if (second st) (signal 'error) (pop st)))))

(defun rpn ()
  (let ((s (read-line *standard-input* nil)))
    (unless (null s)
      (handler-case (eval-rpn (nsubstitute #\Space #\Tab s))
        (:no-error (r) (when r (write (float r)) (terpri)))
        (error () (write-line "error")))