Category Archives: Uncategorized

The HP67 emulator, defining operation keys

We’ve build the basic building blocks of our system to define operation keys.  Now, we put it together.

First, we’ll need a structure to hold the definition of a key.  This will include the position of the key in the interface layout, in a yet-to-be-determined format, a unique ID, an indication of whether a key is available in run-mode, program-mode, or both, a text abbreviation for the key that might be used for a text-based UI, the function it invokes, and a documentation string.  Here’s the initial form of the structure:
calc1.lisp

(defstruct (key-struct)
  (key-location         nil)
  (key-id               nil)
  (avail-modes          :RUN-MODE)
  (abbrev               nil)
  (run-mode-fcn         nil)
  (doc-string           nil))

We’ll also need some forms to hold the key structures and hand out unique IDs:
calc1.lisp

(let ((keys '())
      (next-id 0))

  (defun make-new-id ()
    (prog1
        next-id
      (incf next-id)))

  (defun register-key-structure (ks)
    (let ((this-id (key-struct-key-id ks)))
      (when (>= this-id next-id)
        (setf next-id (1+ this-id))))
    (push ks keys))

  (defun get-keys ()
    keys))

The HP-67 calculator also has a register called the “Last X” register, for use in undoing keypress mistakes.  It holds the value of X as it was before any operation that can destroy the X register.  Most operation keys will update this value, so an appropriate form is added to the rules expansion function we saw earlier.  That function is now as follows:
calc1.lisp

(defun expand-rules (rules-list &key update-last-x)
  (let* ((varnames '(X Y Z W))
         (stack-var (gensym))
         (state-var (gensym))
         (vars-used (get-vars-used rules-list
                                   varnames))
         (vars-assigned (get-vars-assigned rules-list
                                           varnames)))

    ;; If this is an implicit X <- form, make it explicit so the setf
    ;; substitution will work later
    (when (and (= 1 (length vars-assigned))
               (not (member '<- (get-symbols-in-list
                                 rules-list)))
               (= 1 (length rules-list)))
      (setf rules-list 
            (append (list (first varnames) '<-)
                    rules-list)))

    ;; We need new symbols to hold the assigned values of the stack
    ;; variables, to avoid side-effects on multiple assignments.
    (let (gensyms-output)
      (dolist (v vars-assigned)
        (declare (ignore v))
        (push (gensym) gensyms-output))

      (setf rules-list 
            (convert-to-setf-forms 
             rules-list vars-assigned gensyms-output))

      `(lambda (,stack-var ,state-var)
         (declare (ignorable ,stack-var ,state-var))
         (labels
             ((to-radians (angle)
                (convert-angle-to-radians 
                 angle 
                 (get-angle-state ,state-var)))
              (from-radians (angle)
                (convert-angle-from-radians 
                 angle 
                 (get-angle-state ,state-var)))
              (to-rational (num)
                (convert-number-to-rational 
                 num 
                 (get-rational-state ,state-var)))
              (to-double-fp (num)
                (coerce num 'double-float)))

           ,(when update-last-x
                  `(update-last-x ,stack-var))
           (let (,@(mapcar #'(lambda (x) 
                               `(,x (pop-stack ,stack-var))) 
                           vars-used)
                 ,@(mapcar #'(lambda (x) 
                               (list x 0))
                           gensyms-output))
             (handler-case
                 (progn
                   ,@rules-list)
               ((or arithmetic-error simple-error) (c)
                 (set-error-state ,stack-var c)
                ,@(mapcar #'(lambda (x)
                              `(setf ,x 0)) 
                          gensyms-output)))

             ,@(mapcar #'(lambda (x)
                           `(push-stack ,stack-var ,x)) 
                       gensyms-output)))))))

Putting this all together, we define a macro that produces a new operation key and puts it in the list of defined keys.  Note that the macro ends with a (values) form so that it doesn’t insert any text into the stream here, as this macro isn’t actually creating functions, it’s just creating and storing data structures.  Here is the macro:
calc1.lisp

(defmacro define-op-key ((&key
                          location
                          (id (make-new-id))
                          (mode :RUN-MODE)
                          abbreviation
                          (updates-last-x t)
                          documentation)
                         &body run-mode-forms)
  (register-key-structure
   (make-key-struct :key-location location
                    :key-id id
                    :avail-modes mode
                    :abbrev abbreviation
                    :doc-string documentation
                    :run-mode-fcn
                    (expand-rules `(,run-mode-forms)
                                  :update-last-x updates-last-x)))
  (values))

Here is a sample invocation of this macro:
*slime-repl sbcl*
CL-USER> (define-op-key 
             (:abbreviation "+" 
                            :documentation "Adds X to Y")
           (+ Y X))
NIL

The HP67 emulator, first version of expander

Now, I’ll produce the first version of the expander function.  This takes in a list of rules associated with a keypress, and converts them into a Lisp form.  This function will later be called in a macro to define the key operations.  We’ll be using backticks again, since we’re producing code, and that’s the way to make it readable in the source.

Now, at this point an introductory text usually goes through a few iterations, first making a simple version, then adding in gensyms, condition handlers, labels, and so on.  I’m just going to reproduce the whole thing up front and describe it.  Note that we may decide to change or augment this function in the future if we discover something that needs it.

So, here’s the current version of the expander:
calc1.lisp

;; This is going to change a basic rules list into explicit pops,
;; pushes, and exception handling
(defun expand-rules (rules-list)
  (let* ((varnames '(X Y Z W))
         (stack-var (gensym))
         (state-var (gensym))
         (vars-used (get-vars-used rules-list
                                   varnames))
         (vars-assigned (get-vars-assigned rules-list
                                           varnames)))

    ;; If this is an implicit X <- form, make it explicit so the setf
    ;; substitution will work later
    (when (and (= 1 (length vars-assigned))
               (not (member '<- (get-symbols-in-list
                                 rules-list)))
               (= 1 (length rules-list)))
      (setf rules-list 
            (append (list (first varnames) '<-)
                    rules-list)))

    ;; We need new symbols to hold the assigned values of the stack
    ;; variables, to avoid side-effects on multiple assignments.
    (let (gensyms-output)
      (dolist (v vars-assigned)
        (declare (ignore v))
        (push (gensym) gensyms-output))

      (setf rules-list 
            (convert-to-setf-forms 
             rules-list vars-assigned gensyms-output))

      `(lambda (,stack-var ,state-var)
         (declare (ignorable ,stack-var ,state-var))
         (labels
             ((to-radians (angle)
                (convert-angle-to-radians 
                 angle 
                 (get-angle-state ,state-var)))
              (from-radians (angle)
                (convert-angle-from-radians 
                 angle 
                 (get-angle-state ,state-var)))
              (to-rational (num)
                (convert-number-to-rational 
                 num 
                 (get-rational-state ,state-var)))
              (to-double-fp (num)
                (coerce num 'double-float)))

           (let (,@(mapcar #'(lambda (x) 
                               `(,x (pop-stack ,stack-var))) 
                           vars-used)
                 ,@(mapcar #'(lambda (x) 
                               (list x 0))
                           gensyms-output))
             (handler-case
                 (progn
                   ,@rules-list)
               ((or arithmetic-error simple-error) (c)
                 (set-error-state ,stack-var c)
                ,@(mapcar #'(lambda (x)
                              `(setf ,x 0)) 
                          gensyms-output)))

             ,@(mapcar #'(lambda (x)
                           `(push-stack ,stack-var ,x)) 
                       gensyms-output)))))))

The object returned is a lambda form of two arguments, a stack variable and a state variable.  This will allow us to funcall the form stored in a slot, with the appropriate arguments.

We know that some functions might need state information.  One example of this is the operation of converting an angle to radians so that it can be used in a trigonometric operation.  If the current state of the calculator is set to radians, this does nothing, but if it’s set to degrees or gradians, it has to do a conversion.  I don’t want to clutter the invocation in keypresses with the state variable, it should be invisible when defining keys, so I create a labels form to do the appropriate thing with the correct arguments behind the scenes, leaving the form in the keypress definition as simply:

X <- (sin (to-radians X))   or   X <- (from-radians (asin X))

I also have helper functions to convert between rationals and double-precision floats, optionally controlled by the state variable.

Next, the used variables are popped from the stack, and the assigned variables in their own symbols are initialized to zero.  The rules are then executed in a handler-case.  The purpose for this is to allow the calculator to behave well if a function domain error is encountered (such as division by zero), or an assert failure.  In the event of such an error, the stack is informed of the error and supplied with the condition that was signaled, and the output registers are set to zero.  Execution then finished when the output registers are pushed back onto the stack.

Here is a sample of the generated output when run on our rectangular-to-polar forms:
*slime-repl sbcl*

CL-USER> (expand-rules '(X <- (sqrt (+ (* X X) (* Y Y)))
                         Y <- (atan Y X)))
(LAMBDA (#:G939 #:G940)
  (DECLARE (IGNORABLE #:G939 #:G940))
  (LABELS ((TO-RADIANS (ANGLE)
             (CONVERT-ANGLE-TO-RADIANS ANGLE
              (GET-ANGLE-STATE #:G940)))
           (FROM-RADIANS (ANGLE)
             (CONVERT-ANGLE-FROM-RADIANS ANGLE
              (GET-ANGLE-STATE #:G940)))
           (TO-RATIONAL (NUM)
             (CONVERT-NUMBER-TO-RATIONAL NUM
              (GET-RATIONAL-STATE #:G940)))
           (TO-DOUBLE-FP (NUM)
             (COERCE NUM 'DOUBLE-FLOAT)))
    (LET ((X (POP-STACK #:G939))
          (Y (POP-STACK #:G939))
          (#:G942 0)
          (#:G941 0))
      (HANDLER-CASE
       (PROGN
        (SETF #:G941 (SQRT (+ (* X X) (* Y Y))))
        (SETF #:G942 (ATAN Y X)))
       ((OR ARITHMETIC-ERROR SIMPLE-ERROR) (C)
        (SET-ERROR-STATE #:G939 C) (SETF #:G942 0)
        (SETF #:G941 0)))
      (PUSH-STACK #:G939 #:G942)
      (PUSH-STACK #:G939 #:G941))))

The HP67 emulator, internals and modes

At this point, it’s probably a good idea to talk about modes and calculator internals.  The HP-67 calculator supports several modes that modify behaviour.  Examples are programming mode vs. run mode, output in fixed, scientific, or engineering notation, and angular measures in radians, degrees, or gradians.  It is the angle measures that concern us.  The Lisp programming language, naturally, performs trigonometric operations in radians.  We will need to give the programmer an opportunity to account for other angular units when defining the operations that are associated with a keypress.  Our final form will need to account for this mode.  Our macro will define labels in scope to convert to and from radians according to the currently active mode.

Next, we come to the issue of floating-point roundoff.  The HP-67 calculator was actually implemented using BCD arithmetic internally.  It used a 56-bit register for 14 BCD digits.  10 digits formed the mantissa, 2 formed the exponent, and the remaining two digits recorded the signs of the mantissa and of the exponent.

BCD representations are uncommon now, outside of embedded applications and calculators.  The advantage is that every decimal representation that can be displayed can be exactly represented by the internal state of the calculator, which would not be the case if the internal representation were made using the common binary IEEE-754 representation.

If you use the HP-67 calculator to add .8 to itself 10 times, the result is exactly 8.00.  If you do the same thing at the REPL in SBCL, using single-precision floats, the result is 8.0000009536743.  We’d like our emulator to give the correct answer whenever possible.  A common approach is to throw more bits at the problem, and make use of double-precision floats, but we will avoid that whenever possible.  So, what can we do?  Well, Lisp has the concept of rationals, numeric representations that consist of a ratio of integers.  These are ideal for most uses, we can perform our arithmetic with rationals, and then, if necessary, pare the precision of the result down to the appropriate size for our emulator, exactly the way the calculator would do it.  For instance, if we were computing the ratio (/ 1 5), we would store it internally as that rational, with no loss of precision.  We would convert (/ 1 7) to its 10-digit rational form, 1428571429/10000000000.  If we were computing the square root of 2, which is an irrational number, we would express it as a double-precision number with 10 significant digits, and then convert it to a rational representation.  It would be stored as 707106781/500000000.  For simple arithmetic, we would have exact representations whenever possible.

There is some difficulty in trying to ensure that you work only with rational numbers in Lisp.  Arithmetic operations can take rationals and produce rationals, but most other functions do not.  In SBCL, the following output occurs when you compare two numbers that are mathematically equal:
*slime-repl sbcl*

CL-USER> (< (/ 1 3) (sqrt (/ 1 9)))
T

The further difficulty is that if a Lisp function requires a floating-point argument and it receives a rational, it promotes it to a single-precision float.  This behaviour is explicitly outlined in the CLHS.  Single-precision floats are entirely inappropriate when dealing with 10-digit numbers, so this would give us significant problems.

We will, therefore, provide another labels form to force intermediate values to 10-digit rational representations, and our key definition will include an option to pop values from the stack as double-precision floating-point numbers rather than as rationals.

This rational number representation will be a new mode, selectable by the user if desired.  In most applications, double-precision numbers are probably enough, but when doing accounting-style calculations (remember: money isn’t a floating-point number, it’s integer pennies) it is probably desirable to use rational representations.

The HP67 emulator, converting from the domain-specific language

OK, so now we’ve defined our domain-specific language (DSL), and we’ve written a function to retrieve symbols in a tree.  Now, we need a few more functions.

First, we will need something that tells us which variables are used in the forms, so that we know how to pop the stack.  So, we locate the deepest stack variable used, and declare that all variables up to and including that one are used.  That function is here:
calc1.lisp

(defun get-vars-used (rules-list varnames)
  (let ((symbols-used (get-symbols-in-list rules-list))
        (vlen (length varnames)))
    (dotimes (i vlen)
      (let ((check (nth (- vlen i 1) varnames)))
        (when (member check symbols-used)
          (return-from get-vars-used 
            (subseq varnames 0 (- vlen i))))))))

with sample output:
*slime-repl sbcl*
CL-USER> (get-vars-used '(X <- (sqrt (+ (* X X) (* Y Y)))
                          Y <- (atan Y X))
                        '(X Y Z W))
(X Y)

Next, we’ll need a list of variables assigned.  We look for the special ‘<- symbol in all depths of the tree and collect the variables assigned.  If there are no <- symbols, it’s an implicit assignment to X, so we return that variable name alone.  This function is here:
calc1.lisp
(defun get-vars-assigned (rules-list varnames)
  (let ((rv '()))
    (labels
        ((worker (rl)
           (do ((v rl (cdr v)))
               ((not v) rv)
             (cond
               ((listp (first v))
                (setf rv (append rv (worker (first v)))))
               ((and (symbolp (first v))
                     (eq (second v) '<-))
                (push (first v) rv))))))

      (setf rv (worker rules-list))

      (remove-if #'(lambda (x)
                     (not (member x varnames))) rv)
      (if (not rv)
          (list (first varnames))
          (delete-duplicates rv)))))

with sample output:
*slime-repl sbcl*
CL-USER> (get-vars-assigned '(X <- (sqrt (+ (* X X) (* Y Y)))
                              Y <- (atan Y X))
                            '(X Y Z W))
(Y X)

Our third helpful function will convert <- assignments to proper setf forms, and in so doing will convert the syntax back to proper Lisp.  The following function will do that, but it also changes the names of the targets of the setf.  That is, assignment is made to a different symbol, so that the values of X, Y, Z, W are not overwritten.  This is necessary if you look at one of the forms we want to be able to process:

X <- (sqrt (+ (* X X) (* Y Y)))   Y <- (atan Y X)

If these assignments were done serially, the changed value of X would be used to compute Y, and the wrong answer would be produced.  If we try to do it with a psetf, we will be severely limiting our permitted structures, as we will require exactly one assignment execution, happening simultaneously, and the programmer may not want to be bound by that.  So, by assigning the new values to new symbols, and pushing those back onto the stack, we can ensure that we have full flexibility of syntax.  Here, then, is the function that converts to a list of setf forms:
calc1.lisp

(defun convert-to-setf-forms (rules-list 
                              vars-used 
                              output-varnames)
  (let (rv)
    (do ((pos rules-list (cdr pos)))
        ((not pos) rv)
      (cond
        ((and (member (first pos) vars-used)
              (eq (second pos) '<-)
              (third pos))
         (setf rv
               (append rv 
                       `((setf ,(nth (position (first pos)
                                               vars-used)
                                     output-varnames)
                               ,(third pos)))))
         (setf pos (cddr pos)))
        ((listp (first pos))
         (setf rv 
               (append 
                rv 
                (list 
                 (convert-to-setf-forms (first pos)
                                        vars-used
                                        output-varnames)))))
        (t
         (setf rv (append rv (list (first pos)))))))))

with sample output:
*slime-repl sbcl*
CL-USER> (convert-to-setf-forms '(X <- (sqrt (+ (* X X) (* Y Y)))
                                  Y <- (atan Y X))
                                '(X Y)
                                '(X-OUT Y-OUT))
((SETF X-OUT (SQRT (+ (* X X) (* Y Y)))) (SETF Y-OUT (ATAN Y X)))

You’ll notice the use of a backtick in the convert-to-setf-forms function.  If you are new to Lisp, you can be forgiven for thinking that backticks are “the things that make macros”, but that’s not what they are.  The backtick is the intermediate construct between single-quoted lists and lists built with the list function.  Single-quoted lists are literal collections of symbols, no interpretation of the internal structure beyond the list layout itself is performed: no functions are called and no variables are replaced.  The list function is a function, and as such it evaluates all of its arguments.  Bare symbols are converted to their value, or called as functions, depending on the syntax in which they appear.  The backtick allows the programmer to build lists where some elements are evaluated, and some are not.  It’s possible to do the same with the list function, but it’s much less convenient, and much less readable.  Consider, for instance, these two forms, which do the same thing:
*slime-repl sbcl*
CL-USER> (let ((aval 20) (bval 10)) 
           `(let ((a ,aval)
                  (b ,bval))
              (when (> a b)
                (format t "A is greater than B~%"))))
(LET ((A 20) (B 10))
  (WHEN (> A B) (FORMAT T "A is greater than B~%")))

*slime-repl sbcl*

CL-USER> (let ((aval 20) (bval 10))
           (list 'let (list (list 'a aval)
                            (list 'b bval))
                 (list 'when (list '> 'a 'b)
                       (list 'format t "A is greater than B~%"))))
(LET ((A 20) (B 10))
  (WHEN (> A B) (FORMAT T "A is greater than B~%")))

The form built from the backticks is much easier to interpret and modify.  There is nothing about backticks that declares that they must appear in macros, it’s merely that they are almost always necessary for readable macros, but the function contexts where backticks are helpful are less common.  The programmer will find the backtick of considerable use when building Lisp code in a function, as we are doing here.

The HP67 emulator, continuing the domain-specific language

So, we’ve described our domain-specific language (DSL).  We want something Lisp-like, but with a few additional syntaxes.  First, we have a new assignment construct with the ‘<- symbol when appearing to the right of X, Y, Z, or W.  Also, we have an implicit assignment to X if that new assignment symbol does not appear anywhere in the forms.

This assignment syntax is not lisp-like.  The symbol does not appear in a function context, and the standard Lisp reader would not handle it well.  This is where the use of macros enters.  One important advantage of Lisp macros when designing DSLs is that the body of a macro is not fully parsed.  It has to enter through the standard reader, so parentheses must be balanced, single-quotes for literal lists and double-quotes for strings still behave the same way, and so on, but the symbols themselves are not interpreted.  A macro can manipulate the body forms in many ways before passing them to the eval stage.

We need to characterize the forms we’ve received before we can manipulate them.  To decide whether there is an implicit assignment, we’ll need to walk the expression forms we’ve been given and identify all of the symbols used in the tree (the tree is the list of body forms).  We’ll say that if the ‘<- symbol appears, then there’s at least one of our special assignment functions, so no implicit assignment will be used.  So, our first requirement is a function that returns a list of all symbols in the tree it is passed.  That will be this function, get-symbols-in-list:
calc1.lisp

(defun get-symbols-in-list (rlist)
  (let ((rval '()))
    (dolist (element rlist)
      (cond
        ((listp element)
         (setf rval (append rval (get-symbols-in-list element))))
        ((symbolp element)
         (push element rval))))
    (delete-duplicates rval)))

This function parses the passed list and its sublists, and collects a list of distinct symbols.  Each appears only once.  Here’s the output acting on the factorial form from the previous post:
*slime-repl sbcl*
CL-USER> (get-symbols-in-list
          '((let ((n X)
                  (rv X))
              (assert (and (integerp X) (> X 0)))
              (dotimes (i (1- n))
                (setf rv (* rv (- n i 1))))
              X <- rv)))
(<- LET ASSERT AND INTEGERP X > DOTIMES 1- SETF RV * I N -)