From patchwork Mon Aug 24 22:16:10 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Matt Turner X-Patchwork-Id: 142129 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 79C734BA2E19 for ; Mon, 24 Aug 2026 22:20:19 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 79C734BA2E19 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=eTnGkN49 X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-yx1-xb12d.google.com (mail-yx1-xb12d.google.com [IPv6:2607:f8b0:4864:20::b12d]) by sourceware.org (Postfix) with ESMTPS id 08F354BA79AE for ; Mon, 24 Aug 2026 22:16:28 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 08F354BA79AE 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 08F354BA79AE Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::b12d ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609788; cv=none; b=VslmkXK5aWMfSgMYudmpmRWh1DirRwI6oij7DTKndE76Z2HS4aml8ApM5yUzyHjZT9khMN3FPlAyBTjm5kDI07i8yOYCcdm8sDLfZ/vWdDjeEFYskh2TZgoMTGCo3NRnzKd1v5E5a+hDq2okh9w/Llh9nXlVxEqiv7Om5FjW2Ks= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609788; c=relaxed/simple; bh=3+M3EfKXIHgnxpP7oLRvLscu9WLiSxWvFm1FDk+7CMM=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=wX8Mtn9Fxet2LkrQy1GqtXJJ1W0eVuomHTJD5d907ULCBKBbtncpg/g+xZGNGjMyciISLWkcG30/qj//tD5IW5cB0YVQar4wV0u3uT5IZj2mkJ3IZ8pLe8L0YV5HrkfsJfT+MlqmmvP1BxL2TkoGZYCfQIIFCaQSvkql0KYhhYY= 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=eTnGkN49 DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 08F354BA79AE Received: by mail-yx1-xb12d.google.com with SMTP id 956f58d0204a3-66c744a00edso2660345d50.2 for ; Mon, 24 Aug 2026 15:16:27 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787609787; x=1788214587; 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=FMDYbDoc+2MRpWBoWqwPIRuZP57kRFhckRJ9bDS/3eY=; b=eTnGkN499mZ2iOwfdcwEv0o+ZxxvU85XZuWqq0+s56MZTJ8qfr6DhWcwDvUipsuLut o19fZm6PFxKZEX3vBJnbKwnxWVeRPww6b3boefX4XHQ9J0egTqVpWZ32PDiBrmACmwJI 1KmlXQm7MGPg+PxMvBfMMQv5JX7ygSE+rWGeI+Pk5wAo6329qO90fuSsdPX92Uf8hAFt IR1slpHZ7FMmg8XVjgF4wUDfmsi5kBegWMa5hA6vsOyR+f/xWZnHw1oU2wv98izqxhUH S/HoP9EYtcGmNdlPh1nRpV6fCzODtIVL8L064oWxRvUeUe2NBqLkAgT0A+2JgE2nzZMU GzhA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787609787; x=1788214587; 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=FMDYbDoc+2MRpWBoWqwPIRuZP57kRFhckRJ9bDS/3eY=; b=sNnOx3e2NqoGj2pZ06RzeCOk7h27s8Rsagkm6u17QFbYWtopQb1bzJ9ep66OByq4Ki eBlQOpJSmECeO9t/xwplBijkY+BSUs+HdIt1FnDIko94H/EWwqVNoF+KYH0ov1I4UOKZ xdASLoC8QaNXKX0LvYggTNhLHqKyyg61z6BvrCipBpRQYgom6aUypnf5pyOEqOoT65LZ GgZApomDVTOlGIGuXiLAmlzfjbf9+4EWilXiBPrQjdYv6DHfMq3TQetrvODOSObkgcwE ExWfDz9ucIlX3riiyDTUCbeW+OmunSiaWW37xscmxBAhblyl0M4KQhSAkm6pIFB9tRpG C7Mg== X-Gm-Message-State: AFuF++mld0kJnliKhAsrJfBFTToo2XjowACKMvD1Yijk/EgBbjq8sNX+ IS1TNvJX2DP33B4iQOtGRp3iIecHLdovU7HraOls7mRpMyU63pxVB92IVZVOTA== X-Gm-Gg: AR+sD10u+Yr0okITLQrjbyuJ6Yiv1cYGlhwYMoB63AD9SemQXkog2k332X7jJ73u2eV mO6FWxnJNg8DgfS6wJeuQzlbV9mTfp9BtXgnOo8KyHDWno4KS3ffAa4P7yWBJ0XRzM0lcCs78H/ Rs8c/7AQYQuu3tltwcz2vnb8xkjYY9ocMlntjSTNQN3wa2qXVncj8o/iBLI7QfFBDzafctoWBR6 8wEHt7AqVI2X/pIajJUX+ll+LtxiG7KF3CW6irhPCe+55QENHogN7tDc6gzOc+Psgvgul5xB+KL xljzHDKr122iRomAKKfR4qOlVanhoXOGKCMjLEwD05kx2le5gga8WNKSrAIMgZOXqlCWlnuhDe0 0+KJD8skx9NUQTMIdyCUalGGcBp/bE9RDE5WbQxyyoD9xLUOi4Cm9mM0YKtPnPO4TnumvxfrBtM UN2F4xPZ8pe3fsQokcubTCmBmBiDhcufmwUCRdL7C5V00l1uuk9N+DM6Kx X-Received: by 2002:a05:690e:438d:b0:667:a0f5:345d with SMTP id 956f58d0204a3-66d14a0014bmr574828d50.10.1787609787199; Mon, 24 Aug 2026 15:16:27 -0700 (PDT) Received: from localhost ([67.197.117.177]) by smtp.gmail.com with ESMTPSA id 00721157ae682-84caba455dbsm40395117b3.36.2026.08.24.15.16.26 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 24 Aug 2026 15:16:26 -0700 (PDT) From: Matt Turner To: libc-alpha@sourceware.org Cc: Matt Turner Subject: [PATCH 5/7] stdio-common: Handle subnormal values in the printf format tests Date: Mon, 24 Aug 2026 18:16:10 -0400 Message-ID: <20260824221612.4132669-6-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.9 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 reference implementation used to verify the a and A conversions assumed the value it was given was normal, splitting it into a significand of the full width and an exponent. That is not true below the smallest normal value, where the exponent can go no lower and the significand loses bits from the top instead, which is what makes the leading hexadecimal digit of a subnormal come out as zero. Given a subnormal it would have produced a normalized result such as 0x1p-1070 where we print 0x0.000000000001p-1022. Telling the two apart needs the minimum exponent for the type, which was not among the data the test program supplies, so add it as a MINEXP definition reported in a record of its own next to the working precision. Then stop normalizing once that exponent is reached, and pad the digits produced on the left, as there are no longer enough of them to fill the field on their own. The remaining conversions are unaffected: they work from the value itself and never needed it decomposed. How far the exponent has to be shifted to sit after the significand depends on the working precision, so hold MINEXP as reported and combine the two only once a value is due to be converted, rather than requiring the records to arrive in a particular order. Where no MINEXP record arrives the type has no subnormals and no clamping is applied. None of the values iterated over were subnormal, so this could not be observed. Add DBL_TRUE_MIN and LDBL_TRUE_MIN to cover it, which also exercises the smallest exponent with the remaining conversions. How many bits the leading hexadecimal digit holds varies with the type, one for a 53 bit significand and four for a 64 bit one, so both are needed: the wider case lands on a different exponent than the minimum for the type, with LDBL_TRUE_MIN coming out as 0x0.000000000000001p-16385 rather than at the p-16382 that the leading digit of a normal value would sit at. One sign is enough for either, as nothing in the sign handling depends on the value being subnormal, and the records these produce are among the most expensive in the test suite. Tested on x86_64-linux-gnu, where all 672 results pass. Reviewed-by: Adhemerval Zanella --- .../tst-printf-format-skeleton-double.c | 5 +- .../tst-printf-format-skeleton-ldouble.c | 5 +- stdio-common/tst-printf-format-skeleton.c | 23 ++++++++- stdio-common/tst-printf-format.py | 50 +++++++++++++------ 4 files changed, 62 insertions(+), 21 deletions(-) diff --git ./stdio-common/tst-printf-format-skeleton-double.c ./stdio-common/tst-printf-format-skeleton-double.c index 4dbf3fc1ed..ae3acb00f7 100644 --- ./stdio-common/tst-printf-format-skeleton-double.c +++ ./stdio-common/tst-printf-format-skeleton-double.c @@ -26,10 +26,11 @@ #define REF_FMT ".35e" #define REF_VAL(v) (v) #define PREC DBL_MANT_DIG +#define MINEXP DBL_MIN_EXP typedef double type_t; static const type_t vals[] = - { -HUGE_VAL, -DBL_MAX, -DBL_MIN, -0.0, -NAN, NAN, 0, DBL_MIN, - DBL_MAX, HUGE_VAL }; + { -HUGE_VAL, -DBL_MAX, -DBL_MIN, -0.0, -NAN, NAN, 0, DBL_TRUE_MIN, + DBL_MIN, DBL_MAX, HUGE_VAL }; static const char length[] = ""; #include "tst-printf-format-skeleton.c" diff --git ./stdio-common/tst-printf-format-skeleton-ldouble.c ./stdio-common/tst-printf-format-skeleton-ldouble.c index d959f9dc2d..e323d621bb 100644 --- ./stdio-common/tst-printf-format-skeleton-ldouble.c +++ ./stdio-common/tst-printf-format-skeleton-ldouble.c @@ -27,10 +27,11 @@ #define REF_FMT ".35Le" #define REF_VAL(v) (v) #define PREC LDBL_MANT_DIG +#define MINEXP LDBL_MIN_EXP typedef long double type_t; static const type_t vals[] = - { -HUGE_VAL, -LDBL_MAX, -LDBL_MIN, -0.0, -NAN, NAN, 0, LDBL_MIN, - LDBL_MAX, HUGE_VAL }; + { -HUGE_VAL, -LDBL_MAX, -LDBL_MIN, -0.0, -NAN, NAN, 0, LDBL_TRUE_MIN, + LDBL_MIN, LDBL_MAX, HUGE_VAL }; static const char length[] = "L"; #ifndef TIMEOUT diff --git ./stdio-common/tst-printf-format-skeleton.c ./stdio-common/tst-printf-format-skeleton.c index c880da55e2..6250792d44 100644 --- ./stdio-common/tst-printf-format-skeleton.c +++ ./stdio-common/tst-printf-format-skeleton.c @@ -38,6 +38,9 @@ the argument type handled; usually for floating-point conversions only, but it may be required for 128-bit or wider integer data types as well. + MINEXP [optional] Minimum exponent integer constant. Set to the + *_MIN_EXP value for the argument type handled, so that + subnormal values can be told apart from normal ones. Typedefs: type_t Variadic function argument type. Define to the promoted @@ -79,6 +82,11 @@ #ifndef PREC # define PREC 0 #endif +/* Set to the minimum exponent for the type handled; zero where there is + no such thing, in which case no value can be subnormal. */ +#ifndef MINEXP +# define MINEXP 0 +#endif /* The list of conversions permitted for the '#' flag, the '0' flag, and precision respectively. */ @@ -294,8 +302,13 @@ do_printf_flags (char *fmt, size_t idx, const char *l, char c, type_t val) prec: - is produced at the beginning. Then for each VAL from VALS a block - of records is produced starting with: + is produced at the beginning, followed by this one if MINEXP is + nonzero: + + minexp: + + Then for each VAL from VALS a block of records is produced starting + with: val: @@ -328,6 +341,12 @@ do_test (int argc, char *argv[]) return EXIT_FAILURE; } + if (MINEXP != 0 && printf ("minexp:%i\n", MINEXP) < 0) + { + perror ("printf"); + return EXIT_FAILURE; + } + c = *argv[1]; for (v = 0; v < array_length (vals); v++) { diff --git ./stdio-common/tst-printf-format.py ./stdio-common/tst-printf-format.py index dc4fbae1da..f1dd3504d1 100644 --- ./stdio-common/tst-printf-format.py +++ ./stdio-common/tst-printf-format.py @@ -286,18 +286,26 @@ 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). +def to_significand(fr, mant_bits, min_exp): + """Split the positive Fraction FR into an integer significand 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.""" + The significand has exactly MANT_BITS bits for a normal value. Below + the smallest normal the exponent cannot go any lower, so it stops at + MIN_EXP and the significand loses bits from the top instead, which is + what makes the leading hexadecimal digit of a subnormal come out zero. + MIN_EXP is None where the format has no subnormals. FR comes from a + value of this format, so the split is exact either way.""" num, den, exp = normalize(fr.numerator, fr.denominator, mant_bits) + if min_exp is not None and exp < min_exp: + # Subnormal: scale back up to the smallest exponent the format has, + # which drops the significand below its full width. + den <<= min_exp - exp + exp = min_exp return num // den, exp -def render_a(fr, mant_bits, prec, alt, upper): +def render_a(fr, mant_bits, min_exp, 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 @@ -305,14 +313,15 @@ def render_a(fr, mant_bits, prec, alt, upper): 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.""" + from 8 to f. A subnormal has no bit to put there and so leads with a + zero, its exponent being the smallest the format provides.""" 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 + mant, exp = to_significand(fr, mant_bits, min_exp) + digits = ("%x" % mant).zfill(1 + nfrac) exp += 4 * nfrac if prec is None: @@ -351,7 +360,7 @@ def render_e(fr, prec, alt, upper, strip): "-" if exp < 0 else "+", abs(exp)) -def convert_float(value, spec, mant_bits, cache): +def convert_float(value, spec, mant_bits, min_exp, cache): """Convert VALUE, an exact Fraction. CACHE memoizes rendered digits for the value currently being converted.""" conv = spec.conv @@ -369,7 +378,8 @@ def convert_float(value, spec, mant_bits, cache): body = cache.get(key) if body is None: if conv in "aA": - body = render_a(fr, mant_bits, spec.prec, spec.alt, upper) + body = render_a(fr, mant_bits, min_exp, 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) @@ -395,11 +405,13 @@ class Block: """The state a run of records shares: the C type they exercise and the single value they all convert.""" - __slots__ = ("mant_bits", "val_text", "value", "special", - "neg_zero", "cache") + __slots__ = ("mant_bits", "type_min_exp", "min_exp", "val_text", + "value", "special", "neg_zero", "cache") def __init__(self): self.mant_bits = 0 + self.type_min_exp = None + self.min_exp = None self.set_value("") def set_value(self, text): @@ -413,6 +425,12 @@ class Block: def interpret(self, conv): """Interpret the value text as the C type conversion CONV takes.""" if conv in FLOAT_CONVS: + # *_MIN_EXP is the exponent of the smallest normal value with + # the radix point before the significand; shift it to sit after + # the significand, as to_significand expects. Where it was not + # reported the type has no subnormals. + self.min_exp = (None if self.type_min_exp is None + else self.type_min_exp - self.mant_bits) lowered = self.val_text.lower() if "inf" in lowered or "nan" in lowered: self.special = "nan" if "nan" in lowered else "inf" @@ -437,7 +455,7 @@ class Block: if self.special is not None: return convert_special(self.special, self.neg_zero, spec) return convert_float(self.value, spec, self.mant_bits, - self.cache) + self.min_exp, self.cache) if conv in INT_CONVS: return convert_int(self.value, spec) if conv == "c": @@ -484,6 +502,8 @@ def main(): if raw.startswith("prec:"): block.mant_bits = int(raw[5:]) + elif raw.startswith("minexp:"): + block.type_min_exp = int(raw[7:]) elif raw.startswith("val:"): block.set_value(raw[4:]) elif raw.startswith("%"):