Skip to content

prin1 prints some double-floats with too few digits to read back the same float #1816

Description

@dg1sbg

prin1 on certain double-floats produces a decimal string that does not read back to the same float — it emits one significant digit too few.

Minimal reproducer

(let* ((d (scale-float (float 8728896881958312 0d0) 9))
       (s (prin1-to-string d))
       (r (read-from-string s)))
  (format t "~&printed:       ~a~%" s)
  (format t "~&reads back as: ~a~%" (prin1-to-string r))
  (format t "~&(eql d r) =>   ~s~%" (eql d r)))

Observed:

printed:       4.469195203562656d18
reads back as: 4.4691952035626563d18
(eql d r) =>   NIL

16 significant digits are printed; 17 are needed for this value.

Expected: (eql d r) is T. CLHS 22.1.3.1.3 requires a float to be printed so that it reads back as the same float, and the printer is otherwise expected to produce the shortest representation that does so.

How common

About 1 in 13000. The sweep below is deterministic — it uses an LCG rather than cl:random, so the identical set of doubles is tested on every build and results are comparable across binaries.

(defun lcg-next (x)
  (mod (+ (* 6364136223846793005 x) 1442695040888963407) (expt 2 64)))

(defun make-double (x)
  (scale-float (float (ldb (byte 53 0) x) 0d0)
               (- (ldb (byte 8 53) x) 128)))

(let ((fails 0) (checked 0) (x 42))
  (dotimes (i 400000)
    (setf x (lcg-next x))
    (let ((d (make-double x)))
      (when (and (typep d 'double-float) (not (zerop d)))
        (incf checked)
        (let* ((s (with-standard-io-syntax (prin1-to-string d)))
               (y (with-standard-io-syntax
                    (let ((*read-default-float-format* 'double-float))
                      (read-from-string s)))))
          (unless (eql d y)
            (incf fails)
            (when (<= fails 5)
              (format t "~&iter ~6d  ~a  reads back as  ~a~%            integer-decode-float => ~s~%"
                      i s y (multiple-value-list (integer-decode-float d)))))))))
  (format t "~&~%checked ~d doubles, ~d failed to round-trip~%" checked fails))

Output — 30 failures out of 400000 checked:

iter   5188  4.469195203562656d18  reads back as  4.4691952035626563d18
            integer-decode-float => (8728896881958312 9 1)
iter  45858  4.44151070753808d18  reads back as  4.4415107075380803d18
            integer-decode-float => (8674825600660312 9 1)
iter  46629  5.16378726698624d19  reads back as  5.1637872669862404d19
            integer-decode-float => (6303451253645312 13 1)
iter  84937  3.407763170278048d18  reads back as  3.4077631702780483d18
            integer-decode-float => (6655787441949312 9 1)
iter  87221  4.296357292048032d18  reads back as  4.2963572920480323d18
            integer-decode-float => (8391322836031312 9 1)

checked 400000 doubles, 30 failed to round-trip

Every failure I captured sits around 1e18–1e19, with integer-decode-float exponents of 9 or 13.

Versions

Reproduces identically — the same 30 values, at the same iteration indices, with the same integer-decode-float output — on both:

  • clasp-boehmprecise-2.7.0-873-g46626382f
  • a build of current main (fc2beb4) carrying unrelated local commits

So this is long-standing rather than a recent regression. macOS arm64, :build-mode :native, :extensions ().

Side effect: ansi-test is flaky because of this

PRINT.DOUBLE-FLOAT.RANDOM (dependencies/ansi-test/printer/print-floats.lsp:276) draws random doubles across the whole magnitude range and checks readability, so at a ~1-in-13000 defect rate it fails some runs and passes others.

I saw exactly that: two runs of ninja -C build ansi-test on builds differing only by a semantics-neutral change gave 27 failures / 0 unexpected, then 28 failures / 1 unexpected (PRINT.DOUBLE-FLOAT.RANDOM), with every other entry identical. Since the target exits non-zero on any unexpected failure, its exit code alone is not a reliable signal — the failure lists have to be compared. PRINT.LONG-FLOAT.RANDOM is already in tools-for-build/ansi-test-expected-failures.sexp; the double-float one is not.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions