Raymond Toy pushed to branch issue-474-print-parse-hex-floats at cmucl / cmucl

Commits:

3 changed files:

Changes:

  • src/code/exports.lisp
    ... ... @@ -1370,7 +1370,8 @@
    1370 1370
     	   "REMOVE-PACKAGE-LOCAL-NICKNAME"
    
    1371 1371
     	   "PACKAGE-LOCALLY-NICKNAMED-BY-LIST")
    
    1372 1372
       ;; Printing and parsing of C-style hex floats
    
    1373
    -  (:export "PRINT-HEX-FLOAT"
    
    1373
    +  (:export "FLOAT-TO-HEX-STRING"
    
    1374
    +	   "WRITE-HEX-FLOAT"
    
    1374 1375
     	   "FORMAT-HEX-FLOAT"
    
    1375 1376
     	   "HEX-PARSE-ERROR"
    
    1376 1377
     	   "PARSE-HEX-FLOAT"))
    

  • src/code/ext-code.lisp
    ... ... @@ -21,100 +21,69 @@
    21 21
     (intl:textdomain "cmucl")
    
    22 22
     
    
    23 23
     
    
    24
    -;;; C-style hex float printer and parser
    
    25
    -(defun print-hex-single-float (val)
    
    26
    -  "Prints a single-float in C-style hex format."
    
    27
    -  (cond ((float-nan-p val)
    
    28
    -	 "nan")
    
    29
    -        ((float-infinity-p val)
    
    30
    -	 (if (plusp val) "inf" "-inf"))
    
    31
    -        ((zerop val)
    
    32
    -	 (if (eql val -0.0f0)
    
    33
    -	     "-0x0.0p+0f" "0x0.0p+0f"))
    
    34
    -        (t
    
    35
    -         (let* ((bits (ldb (byte 32 0) (kernel:single-float-bits val)))
    
    36
    -                (sign (ldb (byte 1 31) bits))
    
    37
    -                (exp-bits (ldb (byte 8 23) bits))
    
    38
    -                (mantissa (ldb (byte 23 0) bits))
    
    39
    -		;; Print lower-case hex digits.
    
    40
    -		(*print-case* :downcase))
    
    41
    -           (if (zerop exp-bits)
    
    42
    -               ;; Subnormal: Leading digit 0, exponent fixed at -126
    
    43
    -               (format nil "~A0x0.~6,'0Xp-126f"
    
    44
    -                       (if (= sign 1) "-" "")
    
    45
    -                       (ash mantissa 1)) ; Align 23 bits to 24 bits (6 hex digits)
    
    46
    -               ;; Normalized: Leading digit 1, exponent bias 127
    
    47
    -               (format nil "~A0x1.~6,'0Xp~Af"
    
    48
    -                       (if (= sign 1) "-" "")
    
    49
    -                       (ash mantissa 1) ; Align 23 bits to 24 bits (6 hex digits)
    
    50
    -                       (- exp-bits 127)))))))
    
    51
    -
    
    52
    -(defun print-hex-double-float (val)
    
    53
    -  "Prints a double-float in C-style hex format."
    
    54
    -  (cond ((float-nan-p val)
    
    55
    -	 "nan")
    
    56
    -        ((float-infinity-p val)
    
    57
    -	 (if (plusp val) "inf" "-inf"))
    
    58
    -        ((zerop val)
    
    59
    -	 (if (eql val -0.0d0)
    
    60
    -	     "-0x0.0p+0" "0x0.0p+0"))
    
    61
    -        (t
    
    62
    -         (multiple-value-bind (hi-bits lo-bits)
    
    63
    -	     (kernel:double-float-bits val)
    
    64
    -           (let* ((hi (ldb (byte 32 0) hi-bits))
    
    65
    -                  (lo (ldb (byte 32 0) lo-bits))
    
    66
    -                  (sign (ldb (byte 1 31) hi))
    
    67
    -                  (exp-bits (ldb (byte 11 20) hi))
    
    68
    -                  ;; Combine 20 bits from high word and 32 bits from low word
    
    69
    -                  (mantissa (logior (ash (ldb (byte 20 0) hi) 32)
    
    70
    -				    lo))
    
    71
    -		  ;; Print lower-case hex digits.
    
    72
    -		  (*print-case* :downcase))
    
    73
    -             (if (zerop exp-bits)
    
    74
    -                 ;; Subnormal: Leading digit 0, exponent fixed at -1022
    
    75
    -                 (format nil "~A0x0.~13,'0Xp-1022"
    
    76
    -                         (if (= sign 1) "-" "")
    
    77
    -                         mantissa)
    
    78
    -                 ;; Normalized: Leading digit 1, exponent bias 1023
    
    79
    -                 (format nil "~A0x1.~13,'0Xp~A"
    
    80
    -                         (if (= sign 1) "-" "")
    
    81
    -                         mantissa ; 52 bits fits 13 hex digits perfectly
    
    82
    -                         (- exp-bits 1023))))))))
    
    24
    +;;;; C-style hex float printer and parser
    
    83 25
     
    
    84
    -;;; PRINT-HEX-FLOAT  -- Public
    
    26
    +;;; FLOAT-TO-HEX-STRING  -- Public
    
    85 27
     ;;;
    
    86 28
     ;;; Return a string representing a single and double-floats in C-style
    
    87 29
     ;;; hex format.
    
    88
    -(defun print-hex-float (float)
    
    89
    -  "Convert FLOAT to C-style hex string.  Infinities are printed as \"-inf\"
    
    90
    -  and \"inf\".  NaN is printed as \"nan\"."
    
    30
    +(defun float-to-hex-string (val &optional at-p)
    
    31
    +  "Prints a single or double float in bit-perfect C-style hex.
    
    32
    +   If AT-P is true, prepends '+' for non-negative finite values."
    
    33
    +  (cond ((ext:float-nan-p val) "nan")
    
    34
    +        ((ext:float-infinity-p val) 
    
    35
    +         (if (plusp val) (if at-p "+inf" "inf") "-inf"))
    
    36
    +        (t
    
    37
    +         (multiple-value-bind (sign exp-bits mantissa bias precision suffix)
    
    38
    +             (typecase val
    
    39
    +               (single-float
    
    40
    +                (let ((bits (ldb (byte 32 0) (kernel:single-float-bits val))))
    
    41
    +                  (values (ldb (byte 1 31) bits)
    
    42
    +                          (ldb (byte 8 23) bits)
    
    43
    +                          (ash (ldb (byte 23 0) bits) 1) ; Align 23 to 6 hex digits
    
    44
    +                          127 6 "f")))
    
    45
    +               (double-float
    
    46
    +                (multiple-value-bind (hi lo) (kernel:double-float-bits val)
    
    47
    +                  (values (ldb (byte 1 31) hi)
    
    48
    +                          (ldb (byte 11 20) hi)
    
    49
    +                          (logior (ash (ldb (byte 20 0) hi) 32) (ldb (byte 32 0) lo))
    
    50
    +                          1023 13 "")))
    
    51
    +               (t (error "Unsupported float type: ~S" (type-of val))))
    
    52
    +           
    
    53
    +           (let ((sign-str (cond ((= sign 1) "-")
    
    54
    +                                 (at-p "+")
    
    55
    +                                 (t ""))))
    
    56
    +             (if (and (zerop exp-bits) (zerop mantissa))
    
    57
    +                 (format nil "~A0x0.0p+0~A" sign-str suffix)
    
    58
    +                 (format nil "~A0x~A.~V,'0Xp~A~A"
    
    59
    +                         sign-str
    
    60
    +                         (if (zerop exp-bits) "0" "1")
    
    61
    +                         precision
    
    62
    +                         mantissa
    
    63
    +                         (if (zerop exp-bits) (1+ (- bias)) (- exp-bits bias))
    
    64
    +                         suffix)))))))
    
    65
    +
    
    66
    +;;; WRITE-HEX-FLOAT -- Public
    
    67
    +;;;
    
    68
    +;;; Writes a float number in C-style hex format to the given stream.
    
    69
    +(defun write-hex-float (float &optional (stream *standard-output*))
    
    70
    +  "Convert FLOAT to C-style hex string and write it to STREAM.
    
    71
    +  Infinities are printed as \"-inf\" and \"inf\".  NaN is printed as
    
    72
    +  \"nan\"."
    
    91 73
       (declare (float float))
    
    92
    -  (etypecase float
    
    93
    -    (single-float (print-hex-single-float float))
    
    94
    -    (double-float (print-hex-double-float float))))
    
    74
    +  (write-string (float-to-hex-string float)
    
    75
    +		stream))
    
    95 76
     
    
    96 77
     ;;; FORMAT-HEX-FLOAT -- Public
    
    97 78
     ;;;
    
    98 79
     ;;; Function that can be used in a FORMAT ~/
    
    99 80
     (defun format-hex-float (stream arg colon-p at-sign-p &optional width)
    
    100 81
       "Formatter for ~/ext:format-hex-float/. 
    
    101
    -   @ forces sign (+/-). Colon modifier is ignored as per request."
    
    82
    +   Uses AT-SIGN-P (@) to force the sign. COLON-P (:) is currently ignored."
    
    102 83
       (declare (ignore width colon-p))
    
    103
    -  (let ((str (if (typep arg 'single-float) 
    
    104
    -                 (print-hex-single-float arg)
    
    105
    -                 (print-hex-double-float arg))))
    
    106
    -    ;; Prepend '+' if @ is used and number isn't negative or special
    
    107
    -    (when (and at-sign-p 
    
    108
    -               (not (ext:float-nan-p arg))
    
    109
    -               (not (ext:float-infinity-p arg))
    
    110
    -               (not (char= (char str 0) #\-)))
    
    111
    -      (write-char #\+ stream))
    
    112
    -    (write-string str stream)))
    
    84
    +  (write-string (float-to-hex-string arg at-sign-p)
    
    85
    +                stream))
    
    113 86
     
    
    114
    -;;; PARSE-HEX-FLOAT -- Public
    
    115
    -;;;
    
    116
    -;;; Parse a C-style float hex strings.  Always returns a double-float.
    
    117
    -;;; Error-checking is enabled for malformed strings.
    
    118 87
     (define-condition hex-parse-error (parse-error)
    
    119 88
       ((text :initarg :text :reader hex-parse-error-text)
    
    120 89
        (message :initarg :message :reader hex-parse-error-message))
    
    ... ... @@ -122,68 +91,14 @@
    122 91
                  (format s "Hex float parse error in ~S: ~A" 
    
    123 92
                          (hex-parse-error-text c) (hex-parse-error-message c)))))
    
    124 93
     
    
    125
    -#+nil
    
    126
    -(defun parse-hex-float (str)
    
    127
    -  "Parses hex floats using scale-float for the exponent. Strictly hex-literal only."
    
    128
    -  (let* ((str (string-trim '(#\Space #\Tab #\Newline #\Return)
    
    129
    -			   (string-downcase str)))
    
    130
    -         (len (length str)))
    
    131
    -    (when (zerop len)
    
    132
    -      (error 'hex-parse-error :text str :message "Empty string"))
    
    133
    -    
    
    134
    -    (let* ((ends-with-f (and (> len 1)
    
    135
    -			     (char= (char str (1- len)) #\f)))
    
    136
    -           (effective-len (if ends-with-f
    
    137
    -			      (1- len) len))
    
    138
    -           (prototype (if ends-with-f
    
    139
    -			  1.0f0 1.0d0))
    
    140
    -           (has-sign (or (char= (char str 0) #\-)
    
    141
    -			 (char= (char str 0) #\+)))
    
    142
    -           (sign (if (and has-sign
    
    143
    -			  (char= (char str 0) #\-))
    
    144
    -		     -1 1))
    
    145
    -           (start (if has-sign 1 0)))
    
    146
    -      
    
    147
    -      (unless (and (<= (+ start 2) effective-len) 
    
    148
    -                   (string= str "0x" :start1 start :end1 (+ start 2)))
    
    149
    -        (error 'hex-parse-error :text str :message "Missing '0x' prefix"))
    
    150
    -      
    
    151
    -      (let ((p-pos (position #\p str :start start :end effective-len)))
    
    152
    -        (unless p-pos
    
    153
    -	  (error 'hex-parse-error :text str :message "Missing exponent 'p'"))
    
    154
    -
    
    155
    -        (let* ((sig-start (+ start 2))
    
    156
    -               (dot-pos (position #\. str :start sig-start :end p-pos))
    
    157
    -               (exp-start (1+ p-pos))
    
    158
    -               ;; Leading hex: digits before the dot
    
    159
    -               (leading-str (subseq str sig-start (or dot-pos p-pos)))
    
    160
    -               ;; Trailing hex: digits after the dot
    
    161
    -               (trailing-str (if dot-pos (subseq str (1+ dot-pos) p-pos) ""))
    
    162
    -               (has-digits (or (plusp (length leading-str))
    
    163
    -			       (plusp (length trailing-str)))))
    
    164
    -          
    
    165
    -          (unless has-digits
    
    166
    -            (error 'hex-parse-error :text str :message "No hex digits in significand"))
    
    167
    -
    
    168
    -          (handler-case
    
    169
    -              (let* ((leading-int (if (string= leading-str "") 0 
    
    170
    -                                      (parse-integer leading-str :radix 16)))
    
    171
    -                     (trailing-len (length trailing-str))
    
    172
    -                     (trailing-int (if (string= trailing-str "") 0 
    
    173
    -                                       (parse-integer trailing-str :radix 16)))
    
    174
    -                     ;; Calculate the significand as a float: leading + (trailing / 16^len)
    
    175
    -                     (significand (float (+ leading-int 
    
    176
    -                                            (/ trailing-int (expt 16 trailing-len)))
    
    177
    -                                         prototype))
    
    178
    -                     ;; The exponent after 'p'
    
    179
    -                     (raw-exponent (parse-integer str :start exp-start :end effective-len)))
    
    180
    -                ;; Use scale-float to apply the binary exponent efficiently
    
    181
    -                (* sign (scale-float significand raw-exponent)))
    
    182
    -            (error (c)
    
    183
    -	      (error 'hex-parse-error :text str :message (format nil "~A" c)))))))))
    
    184
    -
    
    94
    +;;; PARSE-HEX-FLOAT-FROM-STREAM -- Public
    
    95
    +;;;
    
    96
    +;;; Parse a C-style float hex string from a stream.  Invalid formats
    
    97
    +;;; signal an error.  A single-float or double-float may be returned.
    
    185 98
     (defun parse-hex-float-from-stream (stream)
    
    186
    -  "Reads hex float from stream using double-float accumulation and a 6-character exponent buffer."
    
    99
    +  "Reads a C-style hex float number from STREAM.  A single-float or
    
    100
    +  double-float number is returned.  A HEX-PARSE-ERROR is signaled for
    
    101
    +  an invalid format."
    
    187 102
       (let* ((sign 1.0d0)
    
    188 103
              (char (peek-char t stream))) ; Skip whitespace
    
    189 104
         
    
    ... ... @@ -251,6 +166,15 @@
    251 166
                     (float result 1.0f0) 
    
    252 167
                     result)))))))
    
    253 168
     
    
    254
    -(defun parse-hex-float (str)
    
    255
    -  (with-input-from-string (s str)
    
    256
    -    (parse-hex-float-from-stream s)))
    169
    +;;; PARSE-HEX-FLOAT -- Public
    
    170
    +;;;
    
    171
    +;;; Parse a C-style hex float number from either a string or a stream.
    
    172
    +(defun parse-hex-float (obj)
    
    173
    +  "Parse a C-style hex float number from OBJ which is either a string or a stream."
    
    174
    +  (declare (type (or string stream) obj))
    
    175
    +  (etypecase obj
    
    176
    +    (string
    
    177
    +     (with-input-from-string (s obj)
    
    178
    +       (parse-hex-float-from-stream s)))
    
    179
    +    (stream
    
    180
    +     (parse-hex-float-from-stream obj))))

  • tests/extensions.lisp
    ... ... @@ -21,34 +21,45 @@
    21 21
       (:tag :precision)
    
    22 22
       ;; Double Precision (-1022 Cliff)
    
    23 23
       
    
    24
    -  (assert-equal #x0010000000000000 (get-double-bits (ext:parse-hex-float "0x1.0000000000000p-1022")))
    
    25
    -  (assert-equal #x000fffffffffffff (get-double-bits (ext:parse-hex-float "0x0.fffffffffffffp-1022")))
    
    26
    -  (assert-equal #x001f0195cb356b8f (get-double-bits (ext:parse-hex-float "0x1.f0195cb356b8fp-1022")))
    
    24
    +  (assert-equal #x0010000000000000
    
    25
    +		(get-double-bits (ext:parse-hex-float "0x1.0000000000000p-1022")))
    
    26
    +  (assert-equal #x000fffffffffffff
    
    27
    +		(get-double-bits (ext:parse-hex-float "0x0.fffffffffffffp-1022")))
    
    28
    +  (assert-equal #x001f0195cb356b8f
    
    29
    +		(get-double-bits (ext:parse-hex-float "0x1.f0195cb356b8fp-1022")))
    
    27 30
       
    
    28 31
       ;; Single Precision (-126 Cliff)
    
    29 32
       
    
    30
    -  (assert-equal #x00800000 (get-single-bits (ext:parse-hex-float "0x1.000000p-126f")))
    
    31
    -  (assert-equal #x00400000 (get-single-bits (ext:parse-hex-float "0x0.800000p-126f")))
    
    32
    -  (assert-equal #x7f7fffff (get-single-bits (ext:parse-hex-float "0x1.fffffep+127f"))))
    
    33
    +  (assert-equal #x00800000
    
    34
    +		(get-single-bits (ext:parse-hex-float "0x1.000000p-126f")))
    
    35
    +  (assert-equal #x00400000
    
    36
    +		(get-single-bits (ext:parse-hex-float "0x0.800000p-126f")))
    
    37
    +  (assert-equal #x7f7fffff
    
    38
    +		(get-single-bits (ext:parse-hex-float "0x1.fffffep+127f"))))
    
    33 39
     
    
    34 40
     (define-test test-negative-zero
    
    35 41
       (:tag :edge-cases)
    
    36
    -  (assert-equal #x8000000000000000 (get-double-bits (ext:parse-hex-float "-0x0.0p+0")))
    
    37
    -  (assert-equal #x80000000         (get-single-bits (ext:parse-hex-float "-0x0.0p+0f")))
    
    38
    -  (assert-true (typep (ext:parse-hex-float "-0x0.0p+0f") 'single-float)))
    
    42
    +  (assert-equal #x8000000000000000
    
    43
    +		(get-double-bits (ext:parse-hex-float "-0x0.0p+0")))
    
    44
    +  (assert-equal #x80000000
    
    45
    +		(get-single-bits (ext:parse-hex-float "-0x0.0p+0f")))
    
    46
    +  (assert-true (typep (ext:parse-hex-float "-0x0.0p+0f")
    
    47
    +		      'single-float)))
    
    39 48
     
    
    40 49
     (define-test test-subnormal-boundaries
    
    41 50
       (:tag :edge)
    
    42 51
       ;; Test smallest single-float subnormal
    
    43 52
       (let* ((val (kernel:make-single-float 1))
    
    44
    -         (str (ext::print-hex-single-float val))
    
    53
    +         (str (ext:float-to-hex-string val))
    
    45 54
              (parsed (ext:parse-hex-float str)))
    
    46
    -    (assert-equal (get-single-bits val) (get-single-bits parsed)))
    
    55
    +    (assert-equal (get-single-bits val) (get-single-bits parsed)
    
    56
    +		  val str parsed))
    
    47 57
       ;; Test smallest double-float subnormal
    
    48 58
       (let* ((val (kernel:make-double-float 0 1))
    
    49
    -         (str (ext::print-hex-double-float val))
    
    59
    +         (str (ext:float-to-hex-string val))
    
    50 60
              (parsed (ext:parse-hex-float str)))
    
    51
    -    (assert-equal (get-double-bits val) (get-double-bits parsed))))
    
    61
    +    (assert-equal (get-double-bits val) (get-double-bits parsed)
    
    62
    +		  val str parsed)))
    
    52 63
     
    
    53 64
     (define-test test-double-roundtrip
    
    54 65
       (:tag :stress)
    
    ... ... @@ -58,9 +69,11 @@
    58 69
                (lo (random #x100000000))
    
    59 70
                (val (kernel:make-double-float hi lo)))
    
    60 71
           (unless (or (ext:float-nan-p val) (ext:float-infinity-p val))
    
    61
    -        (let* ((str (ext::print-hex-double-float val))
    
    72
    +        (let* ((str (ext:float-to-hex-string val))
    
    62 73
                    (parsed (ext:parse-hex-float str)))
    
    63
    -          (assert-equal (get-double-bits val) (get-double-bits parsed)))))))
    
    74
    +          (assert-equal (get-double-bits val)
    
    75
    +			(get-double-bits parsed)
    
    76
    +			val str parsed))))))
    
    64 77
     
    
    65 78
     (define-test test-single-roundtrip
    
    66 79
       (:tag :stress)
    
    ... ... @@ -69,6 +82,8 @@
    69 82
                (bits (if (logbitp 31 bits-raw) (- bits-raw #x100000000) bits-raw))
    
    70 83
                (val (kernel:make-single-float bits)))
    
    71 84
           (unless (or (ext:float-nan-p val) (ext:float-infinity-p val))
    
    72
    -        (let* ((str (concatenate 'string (ext::print-hex-single-float val) "f"))
    
    85
    +        (let* ((str (concatenate 'string (ext:float-to-hex-string val) "f"))
    
    73 86
                    (parsed (ext:parse-hex-float str)))
    
    74
    -          (assert-equal (get-single-bits val) (get-single-bits parsed)))))))
    87
    +          (assert-equal (get-single-bits val)
    
    88
    +			(get-single-bits parsed)
    
    89
    +			val str parsed))))))