From patchwork Mon Aug 24 22:16:09 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Matt Turner X-Patchwork-Id: 142125 X-Patchwork-Delegate: azanella@linux.vnet.ibm.com Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 0637C4BA9006 for ; Mon, 24 Aug 2026 22:17:29 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 0637C4BA9006 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20251104 header.b=GXwrzBv6 X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-yw1-x1136.google.com (mail-yw1-x1136.google.com [IPv6:2607:f8b0:4864:20::1136]) by sourceware.org (Postfix) with ESMTPS id 5AED64BA79A9 for ; Mon, 24 Aug 2026 22:16:26 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 5AED64BA79A9 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=gmail.com ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 5AED64BA79A9 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::1136 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609786; cv=none; b=hrza4EipICFYtDPrkNr3wOKBYc485NyjEUXWe5DuRlLiRp4lf+XgDJkP7C2Wqr3wZohkqMk4smxBzkGAlURQehNuyKgVDYsNEagKXuLz7j7tbHoJdVA5Gx0ofBf06jq9gdQOuP5XwZlw5NajYC10OAuPKcJ1sqxlbIM0GO1uGFk= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609786; c=relaxed/simple; bh=hM3R/pRZVEyA9OtQJXZJNk+0oXEC95vNXap8+gO/B0o=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=ofzOKXVBwW0+Ut5UPKjPA5ootdmKtaD11xCguJOlCIgGZXI6IhsZXXa0UnWNN3Y/0HnqsFl8xBHT/4mWcGxeDs/YXL4rULFVTkB8+LU1+t6mEQe2StAugAg78CKVHaZttuoQsIwpp3TStCGqScgIXGfUmPz+l0wJt69R8Qvk6rE= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20251104 header.b=GXwrzBv6 DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 5AED64BA79A9 Received: by mail-yw1-x1136.google.com with SMTP id 00721157ae682-836ce6cbe1eso59156797b3.1 for ; Mon, 24 Aug 2026 15:16:26 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787609786; x=1788214586; darn=sourceware.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=TGs5yIwSuRMXO2D//aT7zP0uOaxavhIQqdMA+eVrFBw=; b=GXwrzBv6L0MiWiF4IrxMyS9i8XYRRSJNxkDH/giFJeRGf6VuExR+pfP1n4Pefmjgxu eZ5UIa121EvghpZx+4XuHBetif0DCZWveeeF4HT+IEPF6WCu3r9m9sVYSezCGvKPYOGY oJbAbZ7gBRACr/gynx5nCcPsfIKSuNT4u9tk8KcM06fdGh+uuC7r/+qwT+hmp0LYq6st xvKdbCoB3EIec3Iz4ICvsL8kgDflIsBITqjU/wG8llQPeWxKUiyfDXIJK43elw3qjJn3 1slqwZwUIY4MH6vy2yrOFyR34AgpSPeWrQiIfqwdVeQw88z/vZXz2tWiYVZGgGXGkTz3 uEOg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787609786; x=1788214586; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=TGs5yIwSuRMXO2D//aT7zP0uOaxavhIQqdMA+eVrFBw=; b=ssteJ2EHGCwCWOUBn1FHnbNmOX+Imo7sFDeYpUy6/+1vZY6tPkpnYT2Dr8Cx8Bh1ys QSqxQoHNZA0sr8unzYElM3vm6TgThsDu0aZcJ0di2dt6gClBw5bVOcgpQTEyRIxRkFEV Jy2Xzkxjv2qQMxkEm9p4oxq8eMRwNn117NO4kGXPCfRTvgeshqDLyutWRIZw2fKREg6F QtvuBi8HioC1Y2PuBvbzS91HNRDNtpXWdiHRnVflTTDDMzs928Lh++EK26TKZZv+/X8O OYMR+e24noGMWYoaGyi9QnjRTN4LFa9L5oELpiQgdy3jaPJ4/DvA37xc0hgewk5aENAc gPMA== X-Gm-Message-State: AFuF++lUA8k3uNafoh1Ums8Chxc1IN/13LelVZFXU0gB/4y5hQbWy0rF bbZLTvXP809x2EKZharn6vSFw3/E737jFgjAyTch37WKTGZiXA8WVN/eJcGHxw== X-Gm-Gg: AR+sD12WAoIDTHxEwuvuBQlmFMCr+1afg+x23B3FXmeMjByWBnbdvFUYCSRIvqVgYLu sw50Xb/OkNrvFS3UiVl7Zz/iBDA+ZRkVfX2xMeDCalGcWGF5K8mvJmoxkQwcWvDNBYe6kBaAKN1 xFZE+Ei0FJSRTP0s49BdPhnVVRaM9sIJjLWjkDNBrBauSGhadDdMY2y8eLDLj0FxB1MbktPDOFP iCGBMFy8F8DRauoD4IXFfm/Mdrom4PWRrQqqMcbmq5ewyVOa1X83w9J+alMPidwRgKX3+Fb+ay7 r3ps04SIt2yEQH9tMIIEWXAgbU5gcknfvSjFDi/ObqvdpFjXm4MqOANefCCulV40GZYP2YUMs6X zRiGMERg7jQfgmyV74U4s2phKoCvsdSSDBxrwvfflONBpy4lwkcz0j725JHjUgVAblvsNqvwIm0 bXTjuP8g5eBdRTBWHq/KJg88e2Zap5vBresgS2NuQextVk9jWHAFYktBgW X-Received: by 2002:a05:690c:e649:20b0:845:23cf:7c01 with SMTP id 00721157ae682-849f14c337dmr90701617b3.10.1787609785646; Mon, 24 Aug 2026 15:16:25 -0700 (PDT) Received: from localhost ([67.197.117.177]) by smtp.gmail.com with ESMTPSA id 00721157ae682-85426caa5e9sm8390367b3.34.2026.08.24.15.16.24 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 24 Aug 2026 15:16:24 -0700 (PDT) From: Matt Turner To: libc-alpha@sourceware.org Cc: Matt Turner Subject: [PATCH 4/7] stdio-common: Add printf format tests for the a and A conversions Date: Mon, 24 Aug 2026 18:16:09 -0400 Message-ID: <20260824221612.4132669-5-mattst88@gmail.com> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260824221612.4132669-1-mattst88@gmail.com> References: <20260824221612.4132669-1-mattst88@gmail.com> MIME-Version: 1.0 X-Spam-Status: No, score=-10.8 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, FREEMAIL_ENVFROM_END_DIGIT, FREEMAIL_FROM, GIT_PATCH_0, RCVD_IN_DNSWL_NONE, RCVD_IN_PBL, SPF_HELO_NONE, SPF_PASS, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org 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. Reviewed-by: Adhemerval Zanella --- 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 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":