Over the past few posts we’ve discussed the realization that we should be using rational numbers for most operations. To that end, we wrote code to render a rational as a printable string in any one of the three output modes of the calculator.
We have rewritten some of the stack/memory code so that only rational numbers to 10 digits of precision are stored there. To truncate the rationals at 10 digits, they are parsed as printable 10-digit scientific-notation strings, then the strings are parsed back into exact rational equivalents.
As mentioned earlier, rational numbers are fine for many purposes, but if a function that requires floating point numbers, like sqrt or log, receives a rational number, that number is promoted to a single-precision float. Single-precision floats generally have less than 10 digits of precision internally, so this is a lossy operation that must not be permitted. To avoid this, the stack and memory operations can optionally convert the number they return to a double-precision float before handing it to the caller. The calling environment must explicitly declare whether it will be requesting doubles or rationals. The parsing code in key-structs.lisp can either set all its stores and recalls to rationals, or all to doubles; the syntax does not permit the mixing of those two modes. Therefore, if a rational is requested, and a single-precision or double-precision float is later pushed or stored, an error will be raised, one that is not caught by the handlers in the key forms, as this is a programming bug that must be fixed. In this way, we catch all operations that convert rationals to single-precision floats.
We’ve also written a function to retrieve the parsed form associated with a defined key, for debugging perusal. Reading over the forms produced for the keys we’ve defined so far, some parsing errors were found and the code was corrected.
Here are the two new conditions used by the code that protects against single-precision float errors:
(define-condition invalid-float-arrived (error) ((val :initarg value :reader get-val)) (:documentation "A floating-point number was pushed when rationals were promised. This is a coding bug and should be fixed.") (:report (lambda (c s) (format s "The float value ~A was encountered." (get-val c))))) (define-condition single-precision-float (error) ((val :initarg value :reader get-val)) (:documentation "A single-precision floating-point number was seen. This is below the promised precision of the calculator, and is a programming bug.") (:report (lambda (c s) (format s "The single-precision float value ~A was encountered." (get-val c)))))
Here is the function that is used by stack and memory store operations to validate the passed value:
(defun fix-input-val (val rflag) (when (eq rflag :DOUBLE-FLOAT) (when (eq (type-of val) 'single-float) (error (make-condition 'single-precision-float :value val))) (setf val (rational val))) (unless (rationalp val) (error (make-condition 'invalid-float-arrived :value val))) (round-to-ultimate-precision val))
Here is the function used by stack and memory retrieve operations to convert to double-precision floats if desired:
(defun fix-output-val (val rflag) (if (eq rflag :DOUBLE-FLOAT) (coerce val 'double-float) val))
The current code is in the git repository under the tag v2014-11-10.