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:
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.
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.