printf %a/%A misrounding (C99 compliance violation) when guard digit is 8

Note

This issue has already been reported via Feedback Assistant as FB20512074, but the status is Investigation Complete – Unable to Diagnose with Current Information. Since this bug does not produce a crash log and is therefore difficult to capture through Feedback, I am also posting it here on the Developer Forum to provide additional details and to open discussion.

Description

When formatting floating-point numbers with %a or %A, macOS libc sometimes rounds incorrectly when the guard digit equals 8. This leads to non-conformance with C99’s round-to-nearest, ties-to-even rule.

Steps to Reproduce

#include <stdio.h>

int main(void) {
    // precision 0
    printf("%.0a\n", 1.5);
    printf("%.0a\n", 1.53);
    printf("%.0a\n", 1.55);
    printf("%.0a\n", 1.56);

    // precision 1
    printf("%.1a\n", 0x1.380p+0);
    printf("%.1a\n", 0x1.381p+0);
    printf("%.1a\n", 0x1.382p+0);
    printf("%.1a\n", 0x1.383p+0);

    return 0;
}

Expected Results (per C99/C11)

%.0a with inputs (1.5, 1.53, 1.55, 1.56):
0x2p+0
0x2p+0
0x2p+0
0x2p+0

%.1a with inputs (0x1.380p+0, 0x1.381p+0, 0x1.382p+0, 0x1.383p+0):
0x1.4p+0
0x1.4p+0
0x1.4p+0
0x1.4p+0

Actual Results (macOS observed)

%.0a with inputs (1.5, 1.53, 1.55, 1.56):
0x1p+0
0x2p+0
0x1p+0
0x2p+0

%.1a with inputs (0x1.380p+0, 0x1.381p+0, 0x1.382p+0, 0x1.383p+0):
0x1.3p+0
0x1.4p+0
0x1.3p+0
0x1.4p+0

This shows that values slightly above half are sometimes treated as ties and rounded down incorrectly.

Root Cause Analysis

Inside Libc/gdtoa/FreeBSD/_hdtoa.c, rounding is decided in dorounding():

if ((s0[ndigits] > 8) ||
    (s0[ndigits] == 8 && (s0[ndigits + 1] & 1)))
    adjust = roundup(s0, ndigits);

This logic has two mistakes:

  1. Half detection
    • Correct: When the guard nibble is 8, all lower discarded digits must be checked.
    • Current: Only the LSB of the next nibble is checked (& 1).
    • Consequence: Cases like ...8C... (e.g. 1.55 ≈ 0x1.8C…) are strictly greater than half, but are treated as exact halves and rounded down.
  2. Tie-to-even parity check
    • Correct: For a true half (all lower digits zero), rounding should use the parity of the last kept digit.
    • Current: The code incorrectly uses the parity of the next discarded nibble instead.
    • Consequence: True ties are not rounded to even reliably.

Proposed Fix (behavioral)

if (s0[ndigits] > 8) {
    adjust = roundup(...);  // strictly > half
} else if (s0[ndigits] == 8) {
    if (any_nonzero_tail(s0 + ndigits + 1)) {
        adjust = roundup(...);  // > half
    } else {
        // exact tie: round-to-even
        if (s0[ndigits - 1] & 1)
            adjust = roundup(...);
    }
}

Impact

This bug is not limited to %.0a; it occurs for any precision when the guard nibble is 8. It causes exact halves to round incorrectly and greater-than-half values to be rounded down. The effect is alternating outputs (zigzag) instead of consistent monotonic rounding. This is a C99 compliance violation.

This issue has already been reported via Feedback Assistant as FB20512074

Yeah, Feedback Assistant is the right place for this… well… feedback. I took a look and at your bug and it seems to have got a little lost. I’m gonna see if I can get it back on the right path. I’ll let you know when I learn more.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

printf %a/%A misrounding (C99 compliance violation) when guard digit is 8
 
 
Q