What rebuilding printf taught me about edge cases
Precision, signed overflow, and the tiny assumptions that turn a simple formatter into a useful systems-programming lesson.
The happy path is the smallest part
Converting a positive integer into decimal digits is straightforward. A printf implementation becomes interesting when width, precision, signs, zero padding, length modifiers, and argument promotion all interact.
Most bugs came from formatting while I was still trying to understand the value. The implementation became easier to reason about once parsing, normalization, measurement, and rendering became separate steps.
Do not negate the most negative value
For a signed two's-complement integer, the positive range is one smaller than the negative range. Negating the minimum value in its signed type therefore overflows.
Compute the magnitude in an unsigned type without directly evaluating the impossible positive signed value. The plus-one form keeps every intermediate representable.
uintmax_t magnitude = value < 0
? (uintmax_t)(-(value + 1)) + 1
: (uintmax_t)value;Precision changes the meaning of zero
For integer conversions, an explicitly specified precision of zero suppresses the digits when the value is zero. That empty digit sequence still participates in width and sign decisions.
Zero padding is also ignored when precision is present. If those rules live in separate branches that write directly to the output, they eventually contradict each other. Measuring the final sign, precision zeros, digits, and spaces first avoids that class of bug.
- Read narrow integer arguments as int because of default argument promotion.
- Choose the sign before calculating padding.
- Calculate digit count from the unsigned magnitude.
- Emit only after the complete output width is known.
The real lesson was about interfaces
printf looks like one function, but its format string is a compact language with a long-lived behavioral contract. Reimplementing it is less about clever conversion loops and more about respecting interactions between rules.
That is useful backend practice too: normalize inputs early, make invariants explicit, and test the uncomfortable boundary values before optimizing the pleasant middle.