#lang racket/base
;; Conversion between binary32 and binary64 big-endian external format (byte-vectors) and
;; internal double-precision floating-point numbers, with special attention paid to
;; preservation of the quiet/signaling bit of NaNs, which otherwise is frequently disturbed by
;; hardware-level conversion between single and double precision.

(provide bytes->float
         float->bytes
         bytes->double
         double->bytes)

(require "float.rkt")
(require (only-in racket/math nan? infinite?))

(module binary racket/base
  (provide (all-defined-out))

  (define (binary32-nan-or-inf? bs)
    (and (= (bitwise-bit-field (bytes-ref bs 0) 0 7) #x7f)
         (bitwise-bit-set? (bytes-ref bs 1) 7)))

  (define (binary64-nan-or-inf? bs)
    (and (= (bitwise-bit-field (bytes-ref bs 0) 0 7) #x7f)
         (= (bitwise-bit-field (bytes-ref bs 1) 4 8) #x0f)))

  (define (sign-bit-set? bs)
    (bitwise-bit-set? (bytes-ref bs 0) 0)))

(require (submod "." binary))

(define (bytes->float bs)
  (if (binary32-nan-or-inf? bs)
      (let* ((vf (integer-bytes->integer bs #f #t))
             (signexp (bitwise-bit-field vf 23 32))
             (payload (bitwise-bit-field vf 0 23))
             (vd (bitwise-ior (arithmetic-shift signexp 55)
                              #x0070000000000000
                              (arithmetic-shift payload 29)))
             (dbs (integer->integer-bytes vd 8 #f #t)))
        (float (floating-point-bytes->real dbs #t 0 8)))
      (float (floating-point-bytes->real bs #t 0 4))))

(define (float->bytes v)
  (let ((v (float-value v)))
    (if (or (nan? v) (infinite? v))
        (let* ((dbs (real->floating-point-bytes v 8 #t))
               (vd (integer-bytes->integer dbs #f #t))
               (signexp (bitwise-bit-field vd 55 64))
               (payload (bitwise-bit-field vd 29 52))
               (vf (bitwise-ior (arithmetic-shift signexp 23)
                                payload))
               (bs (integer->integer-bytes vf 4 #f #t)))
          bs)
        (real->floating-point-bytes v 4 #t))))

(define (bytes->double bs)
  (floating-point-bytes->real bs #t 0 8))

(define (double->bytes v)
  (real->floating-point-bytes v 8 #t))

(module+ test
  (require rackunit)
  (require file/sha1)

  (define (check-roundtrip-double hex)
    (check-equal? (bytes->hex-string (double->bytes (bytes->double (hex-string->bytes hex))))
                  hex))

  (define (check-roundtrip-float hex)
    (check-equal? (bytes->hex-string (float->bytes (bytes->float (hex-string->bytes hex))))
                  hex))

  (check-roundtrip-double "0123456789abcdef")
  (check-roundtrip-double "7ff0000000000321")
  (check-roundtrip-double "7ff0000000000001")
  (check-roundtrip-double "7ff0000000000000")
  (check-roundtrip-double "fff0000000000321")
  (check-roundtrip-double "fff0000000000001")
  (check-roundtrip-double "fff0000000000000")
  (check-roundtrip-double "7ff8000000000321")
  (check-roundtrip-double "7ff8000000000001")
  (check-roundtrip-double "7ff8000000000000")
  (check-roundtrip-double "fff8000000000321")
  (check-roundtrip-double "fff8000000000001")
  (check-roundtrip-double "fff8000000000000")

  (check-roundtrip-float "01234567")
  (check-roundtrip-float "7f800321")
  (check-roundtrip-float "7f800001")
  (check-roundtrip-float "7f800000")
  (check-roundtrip-float "ff800321")
  (check-roundtrip-float "ff800001")
  (check-roundtrip-float "ff800000")
  (check-roundtrip-float "7fc00321")
  (check-roundtrip-float "7fc00001")
  (check-roundtrip-float "7fc00000")
  (check-roundtrip-float "ffc00321")
  (check-roundtrip-float "ffc00001")
  (check-roundtrip-float "ffc00000")
  )
