[PATCH 4/7] stdio-common: Add printf format tests for the a and A conversions

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Wed Sep 2 16:36:35 GMT 2026



On 24/08/26 19:16, Matt Turner wrote:
> The a and A conversions were left out when the formatted printf output
> tests were added, because gawk produces output that differs from ours,
> using insufficient precision where none has been given and choosing a
> different exponent otherwise.  Verification no longer goes through AWK,
> and computing the reference output directly makes them straightforward,
> so cover them now.
> 
> The significand is written out as it stands, which means the leading
> hexadecimal digit holds whatever bits are left over once the remaining
> ones are grouped into whole digits: one bit for a 53 bit significand, so
> the digit is 1, and four for a 64 bit one, so it runs from 8 to f.
> Rounding to a requested precision can carry out of that digit, in which
> case the result is re-expressed with one digit fewer and the exponent
> raised by four rather than the integer part being widened.  The 0x
> prefix precedes any '0' flag padding, as it does for the integer
> hexadecimal conversions.
> 
> Unlike the remaining floating-point conversions these produce different
> digits for an omitted precision than for one of 6, so key the memoized
> digits on the precision as given rather than as defaulted.
> 
> Tested on x86_64-linux-gnu, where all 672 results pass.

LGTM, thanks.

Reviewed-by: Adhemerval Zanella  <adhemerval.zanella@linaro.org>

> ---
>  stdio-common/Makefile             |   2 +-
>  stdio-common/tst-printf-format.py | 106 ++++++++++++++++++++++++------
>  2 files changed, 88 insertions(+), 20 deletions(-)
> 
> diff --git ./stdio-common/Makefile ./stdio-common/Makefile
> index 537962ad86..b06e42b7e5 100644
> --- ./stdio-common/Makefile
> +++ ./stdio-common/Makefile
> @@ -28,7 +28,7 @@ xprintf-funcs := p as d f s sn v vas vd vf vs vsn
>  
>  # List of data types and formats for individual per-conversion printf tests.
>  fmt-convs := double ldouble
> -fmts := E e F f G g
> +fmts := A a E e F f G g
>  
>  # List of data types grouping all conversions in single printf tests.
>  nonfmt-convs := c char int llong long s short

Ok.

> diff --git ./stdio-common/tst-printf-format.py ./stdio-common/tst-printf-format.py
> index a24ea5d402..dc4fbae1da 100644
> --- ./stdio-common/tst-printf-format.py
> +++ ./stdio-common/tst-printf-format.py
> @@ -39,9 +39,12 @@ import sys
>  from fractions import Fraction
>  
>  # Conversions grouped by the C type of the corresponding argument.
> -FLOAT_CONVS = frozenset("eEfFgG")
> +FLOAT_CONVS = frozenset("aAeEfFgG")
>  INT_CONVS = frozenset("bBdiouxX")
>  
> +# Conversions whose output is in upper case.
> +UPPER_CONVS = frozenset("AEFG")
> +
>  # The conversion specifier selects the base an integer is written in.
>  INT_FORMATS = {"b": "b", "B": "b", "o": "o", "u": "d", "x": "x", "X": "X"}
>  
> @@ -80,18 +83,11 @@ def scale_ratio(num, den, power):
>      return num, den * 10 ** -power
>  
>  
> -def decimal_to_binary(text, mant_bits):
> -    """Convert the decimal string TEXT to the nearest value having MANT_BITS
> -    of significand, returned as an exact Fraction.
> +def normalize(num, den, mant_bits):
> +    """Scale the positive ratio NUM/DEN into [2**(mant_bits-1), 2**mant_bits).
>  
> -    The generator prints reference values with enough digits to reproduce
> -    the original, so this recovers the exact value converted."""
> -    fr = Fraction(text)
> -    if fr == 0:
> -        return fr
> -    neg = fr < 0
> -    num, den = (-fr.numerator if neg else fr.numerator), fr.denominator
> -    # Scale so that 2**(mant_bits-1) <= num/den < 2**mant_bits.
> +    Return the scaled pair along with the exponent EXP that was taken out,
> +    so that the original value is num/den * 2**exp."""
>      exp = num.bit_length() - den.bit_length() - mant_bits
>      if exp >= 0:
>          den <<= exp
> @@ -103,6 +99,21 @@ def decimal_to_binary(text, mant_bits):
>      while num < den << (mant_bits - 1):
>          num <<= 1
>          exp -= 1
> +    return num, den, exp
> +
> +
> +def decimal_to_binary(text, mant_bits):
> +    """Convert the decimal string TEXT to the nearest value having MANT_BITS
> +    of significand, returned as an exact Fraction.
> +
> +    The generator prints reference values with enough digits to reproduce
> +    the original, so this recovers the exact value converted."""
> +    fr = Fraction(text)
> +    if fr == 0:
> +        return fr
> +    neg = fr < 0
> +    num, den = (-fr.numerator if neg else fr.numerator), fr.denominator
> +    num, den, exp = normalize(num, den, mant_bits)
>      mant = round_ratio(num, den)
>      if mant >> mant_bits:
>          mant >>= 1
> @@ -256,7 +267,7 @@ def convert_special(kind, neg, spec):
>      """Convert an infinity or a NaN.  The field is padded with spaces
>      whether or not the '0' flag was given, and the alternative form has no
>      effect."""
> -    body = kind.upper() if spec.conv in "EFG" else kind
> +    body = kind.upper() if spec.conv in UPPER_CONVS else kind
>      return spec.pad(body, zero_ok=False, sign=spec.sign_of(neg))
>  
>  
> @@ -275,6 +286,57 @@ def render_f(fr, prec, alt, strip):
>      return whole
>  
>  
> +def to_significand(fr, mant_bits):
> +    """Split the positive Fraction FR into an integer significand of exactly
> +    MANT_BITS bits and a power of two, returned as (significand, exponent).
> +
> +    FR comes from a value of that many significand bits, so the split is
> +    exact.  Subnormals are not among the values the generator uses and are
> +    not handled here."""
> +    num, den, exp = normalize(fr.numerator, fr.denominator, mant_bits)
> +    return num // den, exp
> +
> +
> +def render_a(fr, mant_bits, prec, alt, upper):
> +    """Render the non-negative Fraction FR in the style of 'a'.
> +
> +    The number of bits the leading hexadecimal digit holds is whatever is
> +    left over once the remaining significand bits are grouped into whole
> +    digits, which is how the significand ends up written out without any
> +    shifting.  For a 53 bit significand that leaves one bit, so the leading
> +    digit is 1; for a 64 bit one it leaves four, so the leading digit runs
> +    from 8 to f."""
> +    lead_bits = ((mant_bits - 1) % 4) + 1
> +    nfrac = (mant_bits - lead_bits) // 4
> +    if fr == 0:
> +        digits, exp = "0" * (1 + nfrac), 0
> +    else:
> +        mant, exp = to_significand(fr, mant_bits)
> +        digits = "%x" % mant
> +        exp += 4 * nfrac
> +
> +    if prec is None:
> +        whole, frac = digits[0], digits[1:].rstrip("0")
> +    elif prec < nfrac:
> +        value = round_ratio(int(digits, 16), 16 ** (nfrac - prec))
> +        if value >= 16 ** (1 + prec):
> +            # Rounding carried out of the leading digit; re-express with
> +            # one digit fewer rather than widening the integer part.
> +            value //= 16
> +            exp += 4
> +        text = ("%x" % value).zfill(1 + prec)
> +        whole, frac = text[0], text[1:]
> +    else:
> +        whole, frac = digits[0], digits[1:].ljust(prec, "0")
> +
> +    # The "0x" prefix is not included: like the one the '#' flag produces
> +    # for the integer hexadecimal conversions it precedes any '0' flag
> +    # padding, so the caller pads it along with the sign.
> +    body = whole + "." + frac if frac or alt else whole
> +    text = "%sp%s%d" % (body, "-" if exp < 0 else "+", abs(exp))
> +    return text.upper() if upper else text
> +
> +
>  def render_e(fr, prec, alt, upper, strip):
>      """Render the non-negative Fraction FR in the style of 'e'."""
>      if fr == 0:
> @@ -289,24 +351,26 @@ def render_e(fr, prec, alt, upper, strip):
>                             "-" if exp < 0 else "+", abs(exp))
>  
>  
> -def convert_float(value, spec, cache):
> +def convert_float(value, spec, mant_bits, cache):
>      """Convert VALUE, an exact Fraction.  CACHE memoizes rendered digits for
>      the value currently being converted."""
>      conv = spec.conv
>      neg = value < 0 or (value == 0 and spec.neg_zero)
>      fr = -value if value < 0 else value
>      prec = 6 if spec.prec is None else spec.prec
> -    upper = conv in "EFG"
> +    upper = conv in UPPER_CONVS
>  
>      # The generator iterates a large number of flag and field width
>      # combinations over each value, so the same digits are called for over
>      # and over.  They depend only on the magnitude, the precision and the
>      # alternative form, which is what keeps the wider types inexpensive
>      # despite the arithmetic being exact.
> -    key = (conv, prec, spec.alt)
> +    key = (conv, spec.prec, spec.alt)
>      body = cache.get(key)
>      if body is None:
> -        if conv in "gG":
> +        if conv in "aA":
> +            body = render_a(fr, mant_bits, spec.prec, spec.alt, upper)
> +        elif conv in "gG":
>              sig = 1 if prec == 0 else prec
>              _, exp = decimal_digits(fr, sig) if fr != 0 else ("", 0)
>              if -4 <= exp < sig:
> @@ -321,7 +385,10 @@ def convert_float(value, spec, cache):
>              body = render_f(fr, prec, spec.alt, strip=False)
>          cache[key] = body
>  
> -    return spec.pad(body, zero_ok=True, sign=spec.sign_of(neg))
> +    sign = spec.sign_of(neg)
> +    if conv in "aA":
> +        sign += "0X" if upper else "0x"
> +    return spec.pad(body, zero_ok=True, sign=sign)
>  
>  
>  class Block:
> @@ -369,7 +436,8 @@ class Block:
>          if conv in FLOAT_CONVS:
>              if self.special is not None:
>                  return convert_special(self.special, self.neg_zero, spec)
> -            return convert_float(self.value, spec, self.cache)
> +            return convert_float(self.value, spec, self.mant_bits,
> +                                 self.cache)
>          if conv in INT_CONVS:
>              return convert_int(self.value, spec)
>          if conv == "c":

Ok.


More information about the Libc-alpha mailing list