From patchwork Mon Aug 24 22:16:06 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Matt Turner X-Patchwork-Id: 142128 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 E3AFC4BA79A4 for ; Mon, 24 Aug 2026 22:19:07 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org E3AFC4BA79A4 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=Ag2JQFnZ X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-yw1-x1131.google.com (mail-yw1-x1131.google.com [IPv6:2607:f8b0:4864:20::1131]) by sourceware.org (Postfix) with ESMTPS id 2720F4BA79A5 for ; Mon, 24 Aug 2026 22:16:22 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 2720F4BA79A5 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 2720F4BA79A5 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::1131 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609782; cv=none; b=q0SnnV4+G7vTW7sbDW//sRyD145nmfwq0ztfPB8/IVIvitB/5z6kVPDibC1nrTMPwwhAAfnc69tG++d9hmWqBvgjy6lTRdIpNwPViC1hFUZAMcUo2fULJGyt4jxqL57uVellINYzsXD9FIrgEgDhsjR8yvOss0c0gT0EuJS7mQ0= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609782; c=relaxed/simple; bh=Ym3DC8Bgd9a12xovVJIcD3rVs5mS1i40bw9NmJaV5GU=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=CIZKWEbS8+C9jLYpi78rg6IfD+YFx15lcOJPdj8eZ4wPKDoyO+vqQI/2hPJUUn2f2unwES+8UmhU3UCJPCpdNTTlxtosDk0hTZQfdaKY5/hEJJ/saqDVw20wps0rqVj9blxL2bnUUR1QIfXQkCjsP+CKQpmLJUV6ToE3AM6J0d4= 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=Ag2JQFnZ DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 2720F4BA79A5 Received: by mail-yw1-x1131.google.com with SMTP id 00721157ae682-836ce6cbe1eso59156027b3.1 for ; Mon, 24 Aug 2026 15:16:22 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787609781; x=1788214581; 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=82hStiJ7g/RcPvp3lW1qFfPJG+vu1Ts891QictOstjY=; b=Ag2JQFnZCJo2NRPO5o2rD0mUgNyS5/qqTlKcPUVTdv2in9WWJQ8D54wPsDqPfQpF9A PhwoxHCLmeXicZtGwiIhSJZqZHPJ4YeDS2rMgGZiidmiQ588tq9rXIGSpGVNImoDyaUh HPjTmmDJKgLOQKAXdRSr5+aPMwFgmtONJSl4nBpqwFvWnrk6YQwUvXl3UVNnXWSy3JMV oAdxQBmDy+pfjfymDFHlXbJSpZKgWQp+D0YQZ1dKdCyimbmvWSoq0p0cYf4sYpLZdhkk aqaCPY4vEFh5eDl0eOkj8DeCnNqsx3N3r85Jg5dbUp9VpCdy/QDoxl57pMVCf+Dh/vbv tLag== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787609781; x=1788214581; 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=82hStiJ7g/RcPvp3lW1qFfPJG+vu1Ts891QictOstjY=; b=U78wvgkbDu+AYboRHfzQ75NSCuUtTm8aZPLbCfIm1I25+4DW2oO8WL0st85f2HHHp0 l9kyRKD6HhSdDzbZFBPDTBeiG5IfJIovj+ZTSnbkNzxwG+HVbgumUE2dl3zb2OlAZWfz EIAKo+z4q1W/odgh/AKPKrACUlYWAj4QxS2AlTEjfmNtqU1SJp3P/p3BnskN8OkxWrBs RwfoDzJY7DYlvLeK8obBIRujWrX0rF25H+xHzsz3Q8DkuhUb/o4kpGDiAmNYQoAEINKz fwUMUMmg6o9m1apHU55nXk99k2UZsHieXAvJcy0ZwKbxlZEn4OxMExhirlK0yEonj+oQ 9Llg== X-Gm-Message-State: AFuF++lW5M7odIlNexAx7n8RW7D4uKmLpHdPtQti4GW/c4digPb1PNid R7jPHIbzFf3fjmXyFHjlaTU5CG6b0kASPZPopoQX3dX9HeCaVbHPap8D0zkc8Q== X-Gm-Gg: AR+sD11N9eCC8gUPMy6kKE5RC39f+kwekJe5OyRdlPRYYW3x3LMeS9c2tWTg6FMTTOv Yw1KhUH/sW/5vDjG0wbVeutUlCbYbaNEhDCURo5c1VcwxZgAQlp1+P/bIRGVc+swJmdpDZRMr2r cgdIMXGFdYeDtEQQ6lSH0uZ0CM5w0TnmnwyoOco4WT1pyxOh67FFmB7OkbrcVq+DPTXWEyRo+fu iH4V+allRr1YuIziKcam0datRsVzMHI2sszFNX9GA+MrDsA6XRHajowan7LDGoRvzD+HLhRlF8z Hic1EulObfMFUxGm4c1aka+Gj4Z702dKn0hEnyFRcZ3+MfC1ZbxBFHDPLtcJsQRwWffjRunUfCu ho8DZPH0iACtXsiZqtcZl63tRiAnqs0qVsJFczrAFio3VfSa3T9KNycyHeYCYbAS0KbzVGfBbAW 5zAolF2Cv+YQfvqv1Fstbp40MTqNgU67OvuTTHsR1WyXfTFIRDHpACs2WENrHHw8HBwoXnjZ467 ERj99k= X-Received: by 2002:a05:690e:454f:10b0:66d:16dc:8ed9 with SMTP id 956f58d0204a3-66d16dc9040mr329556d50.23.1787609780704; Mon, 24 Aug 2026 15:16:20 -0700 (PDT) Received: from localhost ([67.197.117.177]) by smtp.gmail.com with ESMTPSA id 956f58d0204a3-66cf4a0c6c3sm4476600d50.12.2026.08.24.15.16.19 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 24 Aug 2026 15:16:19 -0700 (PDT) From: Matt Turner To: libc-alpha@sourceware.org Cc: Matt Turner Subject: [PATCH 1/7] stdio-common: Verify printf format tests with Python rather than AWK Date: Mon, 24 Aug 2026 18:16:06 -0400 Message-ID: <20260824221612.4132669-2-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=-8.2 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, KAM_SHORT, KAM_SOMETLD_ARE_BAD_TLD, 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 formatted printf output tests verify their records against GNU AWK, relying on it to provide an implementation of format processing that is independent from ours. AWK has to run in the bignum mode for the floating-point conversions, because otherwise it uses the system sprintf(3) internally and we end up verifying our code against itself. That in turn makes gawk compiled with MPFR support a requirement for testing the library at all. Beyond that gawk mishandles a number of cases which the AWK script then has to undo by hand: the extraneous leading 0 produced for the alternative form with the octal conversion, the 0 produced where no characters are expected for the hexadecimal conversions, the missing + and space characters for a zero value with the precision of zero, and a collection of sign, flag and field width anomalies for Inf and NaN values. Each such workaround suppresses whatever we might get wrong in the same place. The a, A, b, and B conversions cannot be verified at all, because gawk either does not handle them or produces different output. Replace the AWK script with an equivalent one written in Python, which is already a requirement for building the library. Rather than calling into any formatting routine it computes the reference output directly, using exact integer and rational arithmetic. Working exactly means the result does not depend on the range or precision of any host floating-point type, so the wider types are handled without arbitrary-precision arithmetic having to be built into the interpreter, and none of the workarounds listed above are needed: the corner cases they cover are computed correctly. The same property removes the reason the a, A, b, and B conversions had to be left out; adding them is left for the commits that follow. Rendered digits are memoized per value, without which the exact arithmetic makes the long double conversions slower than AWK. As the capability probes only ever detected gawk build options, they go away along with the unsupported status they could produce, so the f and F conversions are now always verified rather than silently skipped where gawk was built without them. Drop the corresponding note on MPFR from the installation instructions. Tested on x86_64-linux-gnu, where all 576 results continue to pass. Reviewed-by: Adhemerval Zanella --- INSTALL | 4 - manual/install.texi | 4 - stdio-common/Makefile | 4 +- stdio-common/tst-printf-format-c.sh | 4 +- stdio-common/tst-printf-format-char.sh | 4 +- stdio-common/tst-printf-format-double.sh | 54 +-- stdio-common/tst-printf-format-int.sh | 19 +- stdio-common/tst-printf-format-ldouble.sh | 54 +-- stdio-common/tst-printf-format-llong.sh | 19 +- stdio-common/tst-printf-format-long.sh | 19 +- stdio-common/tst-printf-format-s.sh | 4 +- stdio-common/tst-printf-format-short.sh | 4 +- stdio-common/tst-printf-format-uchar.sh | 4 +- stdio-common/tst-printf-format-uint.sh | 19 +- stdio-common/tst-printf-format-ullong.sh | 19 +- stdio-common/tst-printf-format-ulong.sh | 19 +- stdio-common/tst-printf-format-ushort.sh | 4 +- stdio-common/tst-printf-format.awk | 130 ------- stdio-common/tst-printf-format.py | 440 ++++++++++++++++++++++ 19 files changed, 478 insertions(+), 350 deletions(-) delete mode 100644 stdio-common/tst-printf-format.awk create mode 100644 stdio-common/tst-printf-format.py diff --git ./INSTALL ./INSTALL index bafae84f75..0f3745d3b2 100644 --- ./INSTALL +++ ./INSTALL @@ -522,10 +522,6 @@ build the GNU C Library: version 5.4.1 is the newest verified to work to build the GNU C Library. - Testing the GNU C Library requires 'gawk' to be compiled with - support for high precision arithmetic via the 'MPFR' - multiple-precision floating-point computation library. - * GNU 'bison' 2.7 or later 'bison' is used to generate the 'yacc' parser code in the 'intl' diff --git ./manual/install.texi ./manual/install.texi index fe4a368385..e2f45b8e6f 100644 --- ./manual/install.texi +++ ./manual/install.texi @@ -567,10 +567,6 @@ function, which was introduced in version 3.1.2 of @code{gawk}. As of release time, @code{gawk} version 5.4.1 is the newest verified to work to build @theglibc{}. -Testing the GNU C Library requires @code{gawk} to be compiled with -support for high precision arithmetic via the @code{MPFR} -multiple-precision floating-point computation library. - @item GNU @code{bison} 2.7 or later diff --git ./stdio-common/Makefile ./stdio-common/Makefile index a948bfde8f..537962ad86 100644 --- ./stdio-common/Makefile +++ ./stdio-common/Makefile @@ -633,7 +633,7 @@ $(objpfx)tst-printf.out: tst-printf.sh $(objpfx)tst-printf # We can't split a quoted recipe line, so do it via an auxiliary variable. make-tst-printf-format-out = \ - AWK='$(AWK)' BASH='$(BASH)' \ + PYTHON='$(PYTHON)' BASH='$(BASH)' \ $(BASH) $< $@ $(common-objpfx) \ '$(run-program-prefix-before-env) \ $(run-program-env) \ @@ -642,7 +642,7 @@ make-tst-printf-format-out = \ $(run-program-prefix-after-env)' $(objpfx)tst-printf-format-%.out: \ tst-printf-format.sh $(foreach c,$(convs),tst-printf-format-$(c).sh) \ - $(foreach f,$(xprintf-srcs),$(objpfx)$(f)) tst-printf-format.awk + $(foreach f,$(xprintf-srcs),$(objpfx)$(f)) tst-printf-format.py $(make-tst-printf-format-out) > $@; \ $(evaluate-test) diff --git ./stdio-common/tst-printf-format-c.sh ./stdio-common/tst-printf-format-c.sh index 73d28c5607..70a8f59204 100644 --- ./stdio-common/tst-printf-format-c.sh +++ ./stdio-common/tst-printf-format-c.sh @@ -23,12 +23,10 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - echo Verifying c (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-c c | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion c output error, first line:\n/") 2>&1 || exit 1 diff --git ./stdio-common/tst-printf-format-char.sh ./stdio-common/tst-printf-format-char.sh index aa4d211126..ac0f273039 100644 --- ./stdio-common/tst-printf-format-char.sh +++ ./stdio-common/tst-printf-format-char.sh @@ -23,8 +23,6 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - status=0 for f in d i; do @@ -32,7 +30,7 @@ for f in d i; do (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-char $f | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 || status=1 done diff --git ./stdio-common/tst-printf-format-double.sh ./stdio-common/tst-printf-format-double.sh index 051e4716c5..3201a419ab 100644 --- ./stdio-common/tst-printf-format-double.sh +++ ./stdio-common/tst-printf-format-double.sh @@ -24,51 +24,15 @@ format=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -# For floating-point formats we need to use the bignum mode even if the -# regular mode would do, because GAWK in the latter mode uses sprintf(3) -# internally to process the conversion requested, so any bug in our code -# would then be verified against itself, defeating the objective of doing -# the verification against an independent implementation. -AWK="env LC_ALL=C ${AWK:-awk} -M" +status=0 -status=77 - -# Verify that AWK can handle the range required. It also catches: -# "gawk: warning: -M ignored: MPFR/GMP support not compiled in" -# message produced where bignum support is not there, which is the -# only indication as the use of '-M' does not affect the exit status -# in this case. -ref="-1.79769313486231570814527423731704357e+308" -val=$(echo "$ref" | $AWK '{ printf "%.35e\n", $1 }' 2>&1) && - test "$val" = "$ref" && status=0 - -test $status -eq 0 || { echo "No working AWK found" && exit $status; } - -# Check for any additional conversions that AWK handles conditionally -# according to its version and/or the environment it has been built in. -# The 'A' and 'a' conversions are not suitable to use at this point, as -# output produced by AWK is different apparently due to a subtlety in -# rounding, so do not try them. -declare -A conversion_disabled -ref="-inf" -for f in f F; do - conversion_disabled[$f]=true - val=$(echo "$ref" | $AWK '{ printf "%'$f'\n", $1 }' 2>&1) && - test "${val^^}" = "${ref^^}" && unset conversion_disabled[$f] -done - -if test "${conversion_disabled[$format]+set}" = set; then - echo Unsupported $format - status=77 -else - echo Verifying $format - (set -o pipefail - ${test_program_prefix} \ - ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-double $format | - $AWK -f tst-printf-format.awk 2>&1 | - head -n 1 | - sed "s/^/Conversion $format output error, first line:\n/") 2>&1 || - status=1 -fi +echo Verifying $format +(set -o pipefail + ${test_program_prefix} \ + ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-double $format | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | + head -n 1 | + sed "s/^/Conversion $format output error, first line:\n/") 2>&1 || + status=1 exit $status diff --git ./stdio-common/tst-printf-format-int.sh ./stdio-common/tst-printf-format-int.sh index e9dcbedc04..983cd9013f 100644 --- ./stdio-common/tst-printf-format-int.sh +++ ./stdio-common/tst-printf-format-int.sh @@ -23,29 +23,14 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - -status=77 - -# Verify that AWK can handle the range required. It also catches: -# "gawk: warning: -M ignored: MPFR/GMP support not compiled in" -# message produced where bignum support is not there, which is the -# only indication as the use of '-M' does not affect the exit status -# in this case. -ref="-2147483648" -for AWK in "$AWK -M" "$AWK"; do - val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue - test "$val" = "$ref" && status=0 && break -done - -test $status -eq 0 || { echo "No working AWK found" && exit $status; } +status=0 for f in d i; do echo Verifying $f (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-int $f | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 || status=1 done diff --git ./stdio-common/tst-printf-format-ldouble.sh ./stdio-common/tst-printf-format-ldouble.sh index 7ee097ac0a..2be2a592e0 100644 --- ./stdio-common/tst-printf-format-ldouble.sh +++ ./stdio-common/tst-printf-format-ldouble.sh @@ -24,51 +24,15 @@ format=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -# For floating-point formats we need to use the bignum mode even if the -# regular mode would do, because GAWK in the latter mode uses sprintf(3) -# internally to process the conversion requested, so any bug in our code -# would then be verified against itself, defeating the objective of doing -# the verification against an independent implementation. -AWK="env LC_ALL=C ${AWK:-awk} -M" +status=0 -status=77 - -# Verify that AWK can handle the range required. It also catches: -# "gawk: warning: -M ignored: MPFR/GMP support not compiled in" -# message produced where bignum support is not there, which is the -# only indication as the use of '-M' does not affect the exit status -# in this case. -ref="-1.18973149535723176508575932662800702e+4932" -val=$(echo "$ref" | $AWK '{ PREC=113; printf "%.35e\n", $1 }' 2>&1) && - test "$val" = "$ref" && status=0 - -test $status -eq 0 || { echo "No working AWK found" && exit $status; } - -# Check for any additional conversions that AWK handles conditionally -# according to its version and/or the environment it has been built in. -# The 'A' and 'a' conversions are not suitable to use at this point, as -# output produced by AWK is different apparently due to a subtlety in -# rounding, so do not try them. -declare -A conversion_disabled -ref="-inf" -for f in f F; do - conversion_disabled[$f]=true - val=$(echo "$ref" | $AWK '{ printf "%'$f'\n", $1 }' 2>&1) && - test "${val^^}" = "${ref^^}" && unset conversion_disabled[$f] -done - -if test "${conversion_disabled[$format]+set}" = set; then - echo Unsupported $format - status=77 -else - echo Verifying $format - (set -o pipefail - ${test_program_prefix} \ - ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ldouble $format | - $AWK -f tst-printf-format.awk 2>&1 | - head -n 1 | - sed "s/^/Conversion $format output error, first line:\n/") 2>&1 || - status=1 -fi +echo Verifying $format +(set -o pipefail + ${test_program_prefix} \ + ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ldouble $format | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | + head -n 1 | + sed "s/^/Conversion $format output error, first line:\n/") 2>&1 || + status=1 exit $status diff --git ./stdio-common/tst-printf-format-llong.sh ./stdio-common/tst-printf-format-llong.sh index 98a79660cc..d90af6f881 100644 --- ./stdio-common/tst-printf-format-llong.sh +++ ./stdio-common/tst-printf-format-llong.sh @@ -23,29 +23,14 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - -status=77 - -# Verify that AWK can handle the range required. It also catches: -# "gawk: warning: -M ignored: MPFR/GMP support not compiled in" -# message produced where bignum support is not there, which is the -# only indication as the use of '-M' does not affect the exit status -# in this case. -ref="9223372036854775807" -for AWK in "$AWK -M" "$AWK"; do - val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue - test "$val" = "$ref" && status=0 && break -done - -test $status -eq 0 || { echo "No working AWK found" && exit $status; } +status=0 for f in d i; do echo Verifying $f (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-llong $f | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 || status=1 done diff --git ./stdio-common/tst-printf-format-long.sh ./stdio-common/tst-printf-format-long.sh index 89ac3302b8..c00f899aa4 100644 --- ./stdio-common/tst-printf-format-long.sh +++ ./stdio-common/tst-printf-format-long.sh @@ -23,29 +23,14 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - -status=77 - -# Verify that AWK can handle the range required. It also catches: -# "gawk: warning: -M ignored: MPFR/GMP support not compiled in" -# message produced where bignum support is not there, which is the -# only indication as the use of '-M' does not affect the exit status -# in this case. -ref="9223372036854775807" -for AWK in "$AWK -M" "$AWK"; do - val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue - test "$val" = "$ref" && status=0 && break -done - -test $status -eq 0 || { echo "No working AWK found" && exit $status; } +status=0 for f in d i; do echo Verifying $f (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-long $f | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 || status=1 done diff --git ./stdio-common/tst-printf-format-s.sh ./stdio-common/tst-printf-format-s.sh index 015c730609..3be2406c6e 100644 --- ./stdio-common/tst-printf-format-s.sh +++ ./stdio-common/tst-printf-format-s.sh @@ -23,12 +23,10 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - echo Verifying s (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-s s | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion s output error, first line:\n/") 2>&1 || exit 1 diff --git ./stdio-common/tst-printf-format-short.sh ./stdio-common/tst-printf-format-short.sh index a7df8b8e6d..b474f65b96 100644 --- ./stdio-common/tst-printf-format-short.sh +++ ./stdio-common/tst-printf-format-short.sh @@ -23,8 +23,6 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - status=0 for f in d i; do @@ -32,7 +30,7 @@ for f in d i; do (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-short $f | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 || status=1 done diff --git ./stdio-common/tst-printf-format-uchar.sh ./stdio-common/tst-printf-format-uchar.sh index 356de4217d..d0d3627e8d 100644 --- ./stdio-common/tst-printf-format-uchar.sh +++ ./stdio-common/tst-printf-format-uchar.sh @@ -23,8 +23,6 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - status=0 for f in o u x X; do @@ -32,7 +30,7 @@ for f in o u x X; do (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-uchar $f | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 || status=1 done diff --git ./stdio-common/tst-printf-format-uint.sh ./stdio-common/tst-printf-format-uint.sh index b496047a49..2c32133212 100644 --- ./stdio-common/tst-printf-format-uint.sh +++ ./stdio-common/tst-printf-format-uint.sh @@ -23,29 +23,14 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - -status=77 - -# Verify that AWK can handle the range required. It also catches: -# "gawk: warning: -M ignored: MPFR/GMP support not compiled in" -# message produced where bignum support is not there, which is the -# only indication as the use of '-M' does not affect the exit status -# in this case. -ref="4294967295" -for AWK in "$AWK -M" "$AWK"; do - val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue - test "$val" = "$ref" && status=0 && break -done - -test $status -eq 0 || { echo "No working AWK found" && exit $status; } +status=0 for f in o u x X; do echo Verifying $f (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-uint $f | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 || status=1 done diff --git ./stdio-common/tst-printf-format-ullong.sh ./stdio-common/tst-printf-format-ullong.sh index f030f66a24..1136f61bf9 100644 --- ./stdio-common/tst-printf-format-ullong.sh +++ ./stdio-common/tst-printf-format-ullong.sh @@ -23,29 +23,14 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - -status=77 - -# Verify that AWK can handle the range required. It also catches: -# "gawk: warning: -M ignored: MPFR/GMP support not compiled in" -# message produced where bignum support is not there, which is the -# only indication as the use of '-M' does not affect the exit status -# in this case. -ref="18446744073709551615" -for AWK in "$AWK -M" "$AWK"; do - val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue - test "$val" = "$ref" && status=0 && break -done - -test $status -eq 0 || { echo "No working AWK found" && exit $status; } +status=0 for f in o u x X; do echo Verifying $f (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ullong $f | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 || status=1 done diff --git ./stdio-common/tst-printf-format-ulong.sh ./stdio-common/tst-printf-format-ulong.sh index 7102575ed2..15802cec63 100644 --- ./stdio-common/tst-printf-format-ulong.sh +++ ./stdio-common/tst-printf-format-ulong.sh @@ -23,29 +23,14 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - -status=77 - -# Verify that AWK can handle the range required. It also catches: -# "gawk: warning: -M ignored: MPFR/GMP support not compiled in" -# message produced where bignum support is not there, which is the -# only indication as the use of '-M' does not affect the exit status -# in this case. -ref="18446744073709551615" -for AWK in "$AWK -M" "$AWK"; do - val=$(echo "$ref" | $AWK '{ printf "%d\n", $1 }' 2>&1) || continue - test "$val" = "$ref" && status=0 && break -done - -test $status -eq 0 || { echo "No working AWK found" && exit $status; } +status=0 for f in o u x X; do echo Verifying $f (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ulong $f | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 || status=1 done diff --git ./stdio-common/tst-printf-format-ushort.sh ./stdio-common/tst-printf-format-ushort.sh index 5f612b5398..b08ae2906c 100644 --- ./stdio-common/tst-printf-format-ushort.sh +++ ./stdio-common/tst-printf-format-ushort.sh @@ -23,8 +23,6 @@ xprintf=$1; shift common_objpfx=$1; shift test_program_prefix=$1; shift -AWK="env LC_ALL=C ${AWK:-awk}" - status=0 for f in o u x X; do @@ -32,7 +30,7 @@ for f in o u x X; do (set -o pipefail ${test_program_prefix} \ ${common_objpfx}stdio-common/tst-printf-format-${xprintf}-ushort $f | - $AWK -f tst-printf-format.awk 2>&1 | + ${PYTHON:-python3} tst-printf-format.py 2>&1 | head -n 1 | sed "s/^/Conversion $f output error, first line:\n/") 2>&1 || status=1 done diff --git ./stdio-common/tst-printf-format.awk ./stdio-common/tst-printf-format.awk deleted file mode 100644 index 57bea12621..0000000000 --- ./stdio-common/tst-printf-format.awk +++ /dev/null @@ -1,130 +0,0 @@ -# Testing of printf conversions. -# Copyright (C) 2024-2026 Free Software Foundation, Inc. -# This file is part of the GNU C Library. - -# The GNU C Library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. - -# The GNU C Library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. - -# You should have received a copy of the GNU Lesser General Public -# License along with the GNU C Library; if not, see -# . - -BEGIN { - FS = ":" -} - -/^prec:/ { - PREC = $2 - next -} - -/^val:/ { - val = $2 - # Prepend "+" for +Inf or +NaN value lacking a sign, because gawk - # interpretes them as strings rather than numeric values in the - # non-bignum mode unless a sign has been explicitly given. Keep - # original 'val' for reporting. - value = gensub(/^(INF|NAN|inf|nan)/, "+\\1", 1, val) - # Neither changes between the conversions applied to this value. - value_infnan = value ~ /(INF|NAN|inf|nan)/ - value_zero = value == 0 - next -} - -/^%/ { - # Discard the trailing empty field, used to improve legibility of data. - input = $--NF - format = $1 - width = $2 - precision = "." $(NF - 1) - # Discard any negative precision, which is to be taken as if omitted. - sub(/\.-.*/, "", precision) - # Simplify handling and paste the precision and width specified as - # arguments to '*' directly into the format. - sub(/\.\*/, precision, format) - sub(/\*/, width, format) - # Discard length modifiers. They are only relevant to C data types. - sub(/([DHLjhltz]|wf?[1-9][0-9]*)/, "", format) - # Discard the '#' flag with the octal conversion if output starts with - # 0 in the absence of this flag. In that case no extra 0 is supposed - # to be produced, but gawk prepends it anyway. - if (index(format, "#") && format ~ /#.*o/) - { - tmpfmt = gensub(/#/, "", "g", format) - tmpout = sprintf(tmpfmt, value) - if (tmpout ~ /^ *0/) - format = tmpfmt - } - # Likewise with the hexadecimal conversion where zero value with the - # precision of zero is supposed to produce no characters, but gawk - # outputs 0 instead. - else if (index(format, "#") && format ~ /#.*[Xx]/) - { - tmpfmt = gensub(/#/, "", "g", format) - tmpout = sprintf(tmpfmt, value) - if (tmpout ~ /^ *$/) - format = tmpfmt - } - # AWK interpretes input opportunistically as a number, which interferes - # with how the 'c' conversion works: "a" input will result in "a" output - # however "0" input will result in "^@" output rather than "0". Force - # the value to be interpreted as a string then, by appending "". - output = sprintf(format, value "") - # Make up for various anomalies with the handling of +/-Inf and +/-NaN - # values and reprint the output produced using the string conversion, - # with the field width carried over and the relevant flags handled by - # hand. - if (value_infnan && format ~ /[EFGefg]/) - { - minus = format ~ /-/ ? "-" : "" - sign = value ~ /-/ ? "-" : format ~ /\+/ ? "+" : format ~ / / ? " " : "" - if (format ~ /^%[^\.1-9]*[1-9][0-9]*/) - width = gensub(/^%[^\.1-9]*([1-9][0-9]*).*$/, "\\1", 1, format) - else - width = "" - output = gensub(/[-+ ]/, "", "g", output) - output = sprintf("%" minus width "s", sign output) - } - # Produce "+" where the '+' flag has been used with a signed integer - # conversion for zero value, observing any field width in effect. - # In that case "+" is always supposed to be produced, but with the - # precision of zero gawk in the non-bignum mode produces any padding - # requested only. - else if (value_zero && format ~ /\+.*[di]/) - { - output = gensub(/^( *) $/, format ~ /-/ ? "+\\1" : "\\1+", 1, output) - output = gensub(/^$/, "+", 1, output) - } - # Produce " " where the space flag has been used with a signed integer - # conversion for zero value. In that case at least one " " is - # supposed to be produced, but with the precision of zero gawk in the - # non-bignum mode produces nothing. - else if (value_zero && format ~ / .*[di]/) - { - output = gensub(/^$/, " ", 1, output) - } - if (output != input) - { - printf "(\"%s\"%s%s, %s) => \"%s\", expected \"%s\"\n", \ - $1, (NF > 2 ? ", " $2 : ""), (NF > 3 ? ", " $3 : ""), val, \ - input, output > "/dev/stderr" - status = 1 - } - next -} - -{ - printf "unrecognized input: \"%s\"\n", $0 > "/dev/stderr" - status = 1 -} - -END { - exit status -} diff --git ./stdio-common/tst-printf-format.py ./stdio-common/tst-printf-format.py new file mode 100644 index 0000000000..d10580ae8a --- /dev/null +++ ./stdio-common/tst-printf-format.py @@ -0,0 +1,440 @@ +#!/usr/bin/python3 +# Verification of formatted printf output. +# Copyright (C) 2024-2026 Free Software Foundation, Inc. +# This file is part of the GNU C Library. +# +# The GNU C Library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# The GNU C Library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with the GNU C Library; if not, see +# . + +"""Verify records of formatted printf output. + +The record stream produced by tst-printf-format-skeleton.c is read from +standard input and each record's output is checked against a reference +value computed here. Diagnostics for records that do not match are written +to standard error and a nonzero exit status is returned. + +This is deliberately an independent implementation of the ISO C format +processing rules: every conversion is computed with exact integer and +rational arithmetic and nothing defers to the C library, so a bug in printf +cannot verify itself. Working exactly also means no dependence on the +range or precision of any host floating-point type, so the wider types are +handled without arbitrary-precision support having to be built into the +interpreter. +""" + +import io +import os +import sys +from fractions import Fraction + +# Conversions grouped by the C type of the corresponding argument. +FLOAT_CONVS = frozenset("eEfFgG") +INT_CONVS = frozenset("diouxX") + +# The conversion specifier selects the base an integer is written in. +INT_FORMATS = {"o": "%o", "u": "%d", "x": "%x", "X": "%X"} + +# Flags and length modifiers accepted in a conversion specification. +FLAG_CHARS = "-+ #0" +LENGTH_CHARS = "hlLqjzt" + +# log10(2), used only to seed the decimal exponent search. +LOG10_2 = 0.3010299956639812 + + +def diagnose(text): + """Report TEXT on standard error. + + Records hold arbitrary bytes, taken as Latin-1 on the way in, so put + them back the same way rather than through whatever encoding the + locale would otherwise select.""" + sys.stderr.buffer.write(text.encode("latin-1") + b"\n") + sys.stderr.buffer.flush() + + +def round_ratio(num, den): + """Round the non-negative ratio NUM/DEN to the nearest integer, with + ties going to even, matching the default rounding mode.""" + quot, rem = divmod(num, den) + rem *= 2 + if rem > den or (rem == den and quot & 1): + quot += 1 + return quot + + +def scale_ratio(num, den, power): + """Return NUM/DEN multiplied by 10**POWER, as a pair of integers.""" + if power >= 0: + return num * 10 ** power, den + 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. + + 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. + exp = num.bit_length() - den.bit_length() - mant_bits + if exp >= 0: + den <<= exp + else: + num <<= -exp + while num >= den << mant_bits: + den <<= 1 + exp += 1 + while num < den << (mant_bits - 1): + num <<= 1 + exp -= 1 + mant = round_ratio(num, den) + if mant >> mant_bits: + mant >>= 1 + exp += 1 + fr = Fraction(mant << exp) if exp >= 0 else Fraction(mant, 1 << -exp) + return -fr if neg else fr + + +def decimal_digits(fr, ndigits): + """Round the positive Fraction FR to NDIGITS significant decimal digits. + + Return the digit string and the decimal exponent X such that the value + is D[0].D[1:] * 10**X, i.e. X is the exponent an 'e' conversion prints.""" + num, den = fr.numerator, fr.denominator + # Seed from the binary magnitude; the estimate is within one decade. + x = int((num.bit_length() - den.bit_length()) * LOG10_2) + while True: + hi_num, hi_den = scale_ratio(num, den, -(x + 1)) + if hi_num < hi_den: + break + x += 1 + while True: + lo_num, lo_den = scale_ratio(num, den, -x) + if lo_num >= lo_den: + break + x -= 1 + digits = round_ratio(*scale_ratio(num, den, ndigits - 1 - x)) + if digits >= 10 ** ndigits: + # Rounding carried into the next decade, e.g. 9.99 -> 10.0. + digits //= 10 + x += 1 + return str(digits).zfill(ndigits), x + + +class Spec: + """A parsed conversion specification.""" + + __slots__ = ("minus", "plus", "space", "alt", "zero", "width", "prec", + "conv", "neg_zero") + + def __init__(self, fmt, args): + """Parse FMT, taking values for any '*' from ARGS in order.""" + self.minus = self.plus = self.space = self.alt = self.zero = False + self.neg_zero = False + i = 1 # skip '%' + while i < len(fmt) and fmt[i] in FLAG_CHARS: + char = fmt[i] + self.minus |= char == "-" + self.plus |= char == "+" + self.space |= char == " " + self.alt |= char == "#" + self.zero |= char == "0" + i += 1 + + self.width = 0 + if i < len(fmt) and fmt[i] == "*": + self.width = int(args.pop(0)) + i += 1 + # A negative field width is a '-' flag with a positive width. + if self.width < 0: + self.minus = True + self.width = -self.width + else: + start = i + while i < len(fmt) and fmt[i].isdigit(): + i += 1 + if i > start: + self.width = int(fmt[start:i]) + + self.prec = None + if i < len(fmt) and fmt[i] == ".": + i += 1 + if i < len(fmt) and fmt[i] == "*": + self.prec = int(args.pop(0)) + i += 1 + # A negative precision is taken as if it were omitted. + if self.prec < 0: + self.prec = None + else: + start = i + while i < len(fmt) and fmt[i].isdigit(): + i += 1 + self.prec = int(fmt[start:i]) if i > start else 0 + + # Length modifiers only select the C type of the argument, which the + # generator has already applied; they do not affect the output. + while i < len(fmt) and fmt[i] in LENGTH_CHARS: + i += 1 + self.conv = fmt[i] + + def pad(self, body, zero_ok=True, sign=""): + """Apply the field width to SIGN followed by BODY.""" + count = self.width - len(sign) - len(body) + if count <= 0: + return sign + body + if self.minus: + return sign + body + " " * count + if self.zero and zero_ok: + return sign + "0" * count + body + return " " * count + sign + body + + def sign_of(self, neg): + """The character introducing a signed conversion of a value whose + sign is NEG, honoring the '+' and space flags.""" + if neg: + return "-" + if self.plus: + return "+" + if self.space: + return " " + return "" + + +def convert_int(value, spec): + conv = spec.conv + if conv in "di": + neg = value < 0 + digits = str(-value if neg else value) + sign = spec.sign_of(neg) + else: + digits = INT_FORMATS[conv] % value + sign = "" + if spec.prec is not None: + # A zero value converted with a precision of zero produces no + # characters at all. + if value == 0 and spec.prec == 0: + digits = "" + digits = digits.rjust(spec.prec, "0") + if spec.alt: + if conv == "o" and not digits.startswith("0"): + digits = "0" + digits + elif conv in "xX" and value != 0: + # The base prefix precedes any '0' flag padding, so it pads + # along with the sign rather than with the digits. + sign = "0x" if conv == "x" else "0X" + # An explicit precision defeats the '0' flag. + return spec.pad(digits, zero_ok=spec.prec is None, sign=sign) + + +def convert_string(value, spec): + if spec.prec is not None: + value = value[:spec.prec] + return spec.pad(value, zero_ok=False) + + +def convert_char(value, spec): + return spec.pad(value[:1], zero_ok=False) + + +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 + return spec.pad(body, zero_ok=False, sign=spec.sign_of(neg)) + + +def render_f(fr, prec, alt, strip): + """Render the non-negative Fraction FR in the style of 'f'.""" + scaled = round_ratio(*scale_ratio(fr.numerator, fr.denominator, prec)) + text = str(scaled).zfill(prec + 1) + if prec: + whole, frac = text[:-prec], text[-prec:] + else: + whole, frac = text, "" + if strip: + frac = frac.rstrip("0") + if frac or alt: + return whole + "." + frac + return whole + + +def render_e(fr, prec, alt, upper, strip): + """Render the non-negative Fraction FR in the style of 'e'.""" + if fr == 0: + digits, exp = "0" * (prec + 1), 0 + else: + digits, exp = decimal_digits(fr, prec + 1) + whole, frac = digits[0], digits[1:] + if strip: + frac = frac.rstrip("0") + mant = whole + "." + frac if frac or alt else whole + return "%s%s%s%02d" % (mant, "E" if upper else "e", + "-" if exp < 0 else "+", abs(exp)) + + +def convert_float(value, spec, 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" + + # 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) + body = cache.get(key) + if body is None: + if conv in "gG": + sig = 1 if prec == 0 else prec + _, exp = decimal_digits(fr, sig) if fr != 0 else ("", 0) + if -4 <= exp < sig: + body = render_f(fr, sig - 1 - exp, spec.alt, + strip=not spec.alt) + else: + body = render_e(fr, sig - 1, spec.alt, upper, + strip=not spec.alt) + elif conv in "eE": + body = render_e(fr, prec, spec.alt, upper, strip=False) + else: + body = render_f(fr, prec, spec.alt, strip=False) + cache[key] = body + + return spec.pad(body, zero_ok=True, sign=spec.sign_of(neg)) + + +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") + + def __init__(self): + self.mant_bits = 0 + self.set_value("") + + def set_value(self, text): + """Begin a run of records converting the value written as TEXT.""" + self.val_text = text + self.value = None + self.special = None + self.neg_zero = False + self.cache = {} # digits memoized by convert_float + + def interpret(self, conv): + """Interpret the value text as the C type conversion CONV takes.""" + if conv in FLOAT_CONVS: + lowered = self.val_text.lower() + if "inf" in lowered or "nan" in lowered: + self.special = "nan" if "nan" in lowered else "inf" + else: + self.value = decimal_to_binary(self.val_text, self.mant_bits) + self.neg_zero = self.val_text.lstrip().startswith("-") + elif conv in INT_CONVS: + self.value = int(self.val_text) + else: + self.value = self.val_text + + def expect(self, spec): + """The output SPEC is required to produce for this block's value.""" + conv = spec.conv + # Which C type the value has follows from the conversion specifier, + # which is not known until the first record of the block, so the + # text is interpreted here rather than where it was read. + if self.value is None and self.special is None: + self.interpret(conv) + spec.neg_zero = self.neg_zero + 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) + if conv in INT_CONVS: + return convert_int(self.value, spec) + if conv == "c": + return convert_char(self.value, spec) + return convert_string(self.value, spec) + + +def check(raw, block): + """Check one record against the value BLOCK holds, reporting a mismatch + on standard error. Return whether the record was in order.""" + # Records are colon separated and end with an empty field, included to + # make the data easier to read. What precedes the output are the format + # and then any width and precision supplied as arguments. + fields = raw.split(":") + fields.pop() + fmt, actual, args = fields[0], fields[-1], fields[1:-1] + + expect = block.expect(Spec(fmt, list(args))) + if expect == actual: + return True + diagnose('("%s"%s, %s) => "%s", expected "%s"' + % (fmt, "".join(", " + arg for arg in args), block.val_text, + actual, expect)) + return False + + +def main(): + # The wider types at the precisions exercised produce integers well past + # the default limit on conversion to a string. Versions that predate + # the limit have no such call and need no lifting. + if hasattr(sys, "set_int_max_str_digits"): + sys.set_int_max_str_digits(0) + + # Records hold arbitrary bytes, so read them as Latin-1 rather than + # through whatever encoding the locale would otherwise select. + stdin = io.TextIOWrapper(sys.stdin.buffer, encoding="latin-1", + newline="\n") + + block = Block() + status = 0 + + for raw in stdin: + raw = raw.rstrip("\n") + + if raw.startswith("prec:"): + block.mant_bits = int(raw[5:]) + elif raw.startswith("val:"): + block.set_value(raw[4:]) + elif raw.startswith("%"): + if not check(raw, block): + status = 1 + else: + diagnose('unrecognized input: "%s"' % raw) + status = 1 + + return status + + +if __name__ == "__main__": + try: + sys.exit(main()) + except BrokenPipeError: + # The wrappers pipe into 'head', which stops reading once it has + # the first diagnostic. Point what is left of standard error at + # the null device, so that flushing at exit does not run into the + # same failure and report it a second time. + os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stderr.fileno()) + sys.exit(1) From patchwork Mon Aug 24 22:16:07 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Matt Turner X-Patchwork-Id: 142124 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 280284BA799E for ; Mon, 24 Aug 2026 22:17:11 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 280284BA799E 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=GYG6h6tK X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-yw1-x1133.google.com (mail-yw1-x1133.google.com [IPv6:2607:f8b0:4864:20::1133]) by sourceware.org (Postfix) with ESMTPS id 20AD04BA79A9 for ; Mon, 24 Aug 2026 22:16:23 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 20AD04BA79A9 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 20AD04BA79A9 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::1133 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609783; cv=none; b=MdJFCYUPSEGYLyhV1RcFa78BB/9kmWPPGxbydgHVKrl66xxjOG+Oa6T3CQjFmkW7NJrtqMJWslaZxEOjP86MrVf4Amd9+wIhmarfuqkZ0bGUiVVHlvFriRal2BDLCqIMbuoIkQP03sl9w2Ufnk2faZIUGA1sk1llJvkHqwfPmVc= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609783; c=relaxed/simple; bh=9DGhQiICQUvLOct3dOpOCdHHMJ3G7p8RO0QT/6zZrg0=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=GKUwr4+EqPVkbiYeQPpOOMiOfZb078lGnBbngH1nMDRqTNOpUAvNQYD72+djzaoGmtoiM92fD4T3egF0Wzl2vQPW74iGLplj6PaxleZZdvL2O7EJ757/b7HMnrPtyuZk1l/rTcK8WwBTUg2m/XKa5hgvhKN8loL04PIx4AqY8Wg= 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=GYG6h6tK DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 20AD04BA79A9 Received: by mail-yw1-x1133.google.com with SMTP id 00721157ae682-84c4d4f68c7so46135017b3.2 for ; Mon, 24 Aug 2026 15:16:23 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787609782; x=1788214582; 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=T+brU6Ply0vdtK2fadxoB7AMBTytpUVn0fWDfS0DKLg=; b=GYG6h6tK5OfOoHY6Akt8FxfMce6Z1mRsIr7kgtfVrVN3ucEawuhE0b8RZHwt9N2tAe 7IH3drnLP6hZ30WOENSDUPZL3RKo9I9oxe5zTL3hbJ5UVLwy/M1gahtdsS+29HN33G1p JBBN4UvvzEFk6lcfd5ZnfdSNFJu3sACBD1sPAcpw38RACXzIca3bjgIsWik74dvkl0LB 1kthDE36v3j9/0+5pPiLB0MjUOoLtHyiXPUtlcOwPrwJztV9uNOsDKrAB1dZHSUgUMA+ 43VTF/rCfVKRKdX6RsaH162kb9BNPgkjV9NWK+m+JoVBxnXs3bgW2comrtD1kbCqqa3p JtnQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787609782; x=1788214582; 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=T+brU6Ply0vdtK2fadxoB7AMBTytpUVn0fWDfS0DKLg=; b=U9BRoL/T0PVIGnfqQBWwGSPF5pzfZBDYaZ7Sde0gvNjLykAjBW4SYk0Cng3PEIzrEK 0txiQhg6QfwHt3kxjdTuViGwaFJq8g/KISaYK8UFuxrr/+zVFA+ibClBJBOS5gnV59M+ HjtzWLYXaljhXhhSdqSiuiE0JZwOMTSSVdgRC1Q+NFUaV0m0wTEi7AUgsQSNgYudre6V WoXNgxRdeRjPhZ4oaxQAOA2ZNY8reAxDvGS+ERQ4t6RzvuXOWRh2v+cUdfCDpHdZKqtp rZHiCTUEzbnq/eEbfTAdrVxCcz1LGXBzo7gGqOIbsnC+oCJ9h5GP53atuGqaOp8bg1Zk hTzA== X-Gm-Message-State: AFuF++lZpcBbX6nkTHRDaSHSfVRKptVRcXIuuac33qb0dNMuOHtpV5hm nwRad/KMlUWwLWDgOlZ3EKxqmyFZSyuG5bHiKAI517Mv6MtII+Jwd9p4vaK5Ow== X-Gm-Gg: AR+sD11ewc/kcAhaC1XChzeFJXokke1bMU4MARA/H0Mi6492Z8d4K0M17Gat+t4waOH rfEk1Cc5WuYepMyTcNeozJAoIcl+PoMIQo5hTmx2jBU9oMk2sh3+KUQQes3FxYdDioti3uVdlbp 0y0VMjPJlEsbQIT8PKnzPupVD2AQzmKpb0CUALN2YEaSiY/Nb0Oyf9Tited0VkX77KL4zEDFdWl 8IU5SaFtMIoAIWgbGzS0upj7OVCE9zO+0q//Tn5nI099B4ckbrTYpjbCFSV4VkFq93dWsDF8tkz grH60WbS+rloZ4fdPq1BcEcFyj6zCazYckeX7Tz9FGwg/6SHOP4zpaOBYiChw/3v8JhvsSMXtH1 E2MdFXmGhk4xEYWKjW0rtDI1rJGqie0AxEgAxS3g7yxNQs8W6FwSRFS40VMOTm56zjivN5lx09a pRXx9eMCtr5h3ob1msu2kbwwSXMudt0/pIgBoQLWhNix5HEnjHi0w0at+KhVlqpkXVUy8= X-Received: by 2002:a05:690c:9987:b0:80b:9f4e:a70 with SMTP id 00721157ae682-849f5a0e03cmr130896227b3.26.1787609782340; Mon, 24 Aug 2026 15:16:22 -0700 (PDT) Received: from localhost ([67.197.117.177]) by smtp.gmail.com with ESMTPSA id 00721157ae682-84cab854483sm40072917b3.33.2026.08.24.15.16.21 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 24 Aug 2026 15:16:21 -0700 (PDT) From: Matt Turner To: libc-alpha@sourceware.org Cc: Matt Turner Subject: [PATCH 2/7] stdio-common: Iterate over the huge width for one printf function only Date: Mon, 24 Aug 2026 18:16:07 -0400 Message-ID: <20260824221612.4132669-3-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.5 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 HUGE_WIDTH is chosen so that none of the strings produced are truncated, which for the floating-point types means every record it takes part in carries hundreds or thousands of digits. Those records dominate the cost of this whole family of tests: for the long double conversions they are 99% of the bytes produced, and the long double targets alone account for 92% of the time the tests take. The digits being checked are produced by the same conversion code whichever of the printf family of functions is used; what differs between the twelve of them is the sink the result is written to, which the smaller widths cover already. Iterate over HUGE_WIDTH for a single function then, chosen as printf, and let the remaining eleven stop at MID_WIDTH. This applies to the double and long double conversions only; for the other types full-precision output is short and costs nothing, so they keep iterating over it as before. Together with the switch to verifying in Python this takes the tests from 935s to 375s of processor time on x86_64-linux-gnu, with all 576 results continuing to pass. What remains is mostly intrinsic: the f and F conversions print the whole integer part regardless of the precision requested, so LDBL_MAX runs to some 4932 digits even at MID_WIDTH. Reviewed-by: Adhemerval Zanella --- stdio-common/tst-printf-format-p.h | 4 ++++ .../tst-printf-format-skeleton-double.c | 2 ++ .../tst-printf-format-skeleton-ldouble.c | 2 ++ stdio-common/tst-printf-format-skeleton.c | 19 ++++++++++++++++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git ./stdio-common/tst-printf-format-p.h ./stdio-common/tst-printf-format-p.h index 48dcc3b48c..c35ca2c44d 100644 --- ./stdio-common/tst-printf-format-p.h +++ ./stdio-common/tst-printf-format-p.h @@ -18,6 +18,10 @@ #include +/* Conversions are verified at the full width and precision through this + function; see the comment on WPHUGE in tst-printf-format-skeleton.c. */ +#define TST_PRINTF_HUGE 1 + #define printf_under_test(...) \ ({ \ int result; \ diff --git ./stdio-common/tst-printf-format-skeleton-double.c ./stdio-common/tst-printf-format-skeleton-double.c index e740c193b5..4dbf3fc1ed 100644 --- ./stdio-common/tst-printf-format-skeleton-double.c +++ ./stdio-common/tst-printf-format-skeleton-double.c @@ -21,6 +21,8 @@ #define MID_WIDTH 20 #define HUGE_WIDTH 320 +/* Full-precision output runs to hundreds of digits here. */ +#define TST_PRINTF_WIDE_TYPE 1 #define REF_FMT ".35e" #define REF_VAL(v) (v) #define PREC DBL_MANT_DIG diff --git ./stdio-common/tst-printf-format-skeleton-ldouble.c ./stdio-common/tst-printf-format-skeleton-ldouble.c index 29bc8b7d36..d959f9dc2d 100644 --- ./stdio-common/tst-printf-format-skeleton-ldouble.c +++ ./stdio-common/tst-printf-format-skeleton-ldouble.c @@ -22,6 +22,8 @@ #define MID_WIDTH 20 #define HUGE_WIDTH 4950 +/* Full-precision output runs to thousands of digits here. */ +#define TST_PRINTF_WIDE_TYPE 1 #define REF_FMT ".35Le" #define REF_VAL(v) (v) #define PREC LDBL_MANT_DIG diff --git ./stdio-common/tst-printf-format-skeleton.c ./stdio-common/tst-printf-format-skeleton.c index 000d8fa50d..3594124513 100644 --- ./stdio-common/tst-printf-format-skeleton.c +++ ./stdio-common/tst-printf-format-skeleton.c @@ -102,6 +102,23 @@ static struct #define STR(v) #v #define WPINIT(v) {0, STR (v)}, {v, NULL}, {-v, NULL} +/* HUGE_WIDTH is chosen so that nothing is truncated, which for the wider + floating-point types means thousands of digits in every record it takes + part in. Those records dominate the run time of this whole family of + tests. The digits they check are produced by the same conversion code + regardless of which of the printf family of functions is used; only the + sink the result is written to differs, and that is covered at the + smaller widths already. So iterate over HUGE_WIDTH for one function + only, chosen as 'printf' by having tst-printf-format-p.h define + TST_PRINTF_HUGE, and let the remaining functions stop at MID_WIDTH. + Types whose full-precision output is short keep it unconditionally, as + it costs nothing there. */ +#if defined TST_PRINTF_WIDE_TYPE && !defined TST_PRINTF_HUGE +# define WPHUGE +#else +# define WPHUGE , WPINIT (HUGE_WIDTH) +#endif + /* Width and precision settings to iterate over; zero is initialized directly as it has no corresponding negated value and other values use the helper above. */ @@ -113,7 +130,7 @@ static struct wp const char *s; } const wp[] = { {0, "0"}, {0, NULL}, WPINIT (1), WPINIT (2), - WPINIT (MID_WIDTH), WPINIT (HUGE_WIDTH) }; + WPINIT (MID_WIDTH) WPHUGE }; /* Produce a record according to '%' and zero or more output format flags already provided in FMT at indices 0..IDX-1, width W if non-NULL, '.' From patchwork Mon Aug 24 22:16:08 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Matt Turner X-Patchwork-Id: 142123 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 04B844BA79A5 for ; Mon, 24 Aug 2026 22:17:06 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 04B844BA79A5 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=Rsn/nZeF X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-yx1-xb130.google.com (mail-yx1-xb130.google.com [IPv6:2607:f8b0:4864:20::b130]) by sourceware.org (Postfix) with ESMTPS id AEAAF4BA79AE for ; Mon, 24 Aug 2026 22:16:24 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org AEAAF4BA79AE 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 AEAAF4BA79AE Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::b130 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609784; cv=none; b=PhHR0wfnEz4hlHqPgWSvltrzyKLxRego115q2wi4wjABS65cTMa840eINNtTTwjexMxY75EzJMXZarjjXD3ndNeJ7axsyzg9Gn2XFZp06eIytucU7K2EphHPvBjIB+UzIb7VuJ4tQ9+pmTm84fLTuCmPHm5NaeyqC2rR73mcAUA= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609784; c=relaxed/simple; bh=VN2XyHRfU+WB3yjclyB1xyRloWmXxiTrH26114YtEJs=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=tu2rVk86ZZYsr+SNuws163o/2w7+2+ZcyHPCwTMHwzpy/TGEtsuiGgyBvUrGNgcaW1ju3atqzln18LJ71x0fQnNi7rhSEIVdT+yMXjQ2aTIt6BscuRFYZUhEeeHsCIKqqVWrf59K1VQlRoUSL28pmH0fKYbpHaMvi66wpNQvBbM= 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=Rsn/nZeF DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org AEAAF4BA79AE Received: by mail-yx1-xb130.google.com with SMTP id 956f58d0204a3-66d0c8d0d6eso1105413d50.1 for ; Mon, 24 Aug 2026 15:16:24 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787609784; x=1788214584; 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=agkHTfuakx1rgRKgGIKM0NPif0KsJOC9FtBiJzgivbg=; b=Rsn/nZeFJP8Z1BYNSunC203BFOyabm37iwHouHFaHUFKOqIbhSSoa4SRR1knwuyquE OciipMPcKDiz6n0Tl2Hf3bSz6qGYnV9b54H4kWuj7kE5qjkN2GeQY26S4COVLENFSXYY Yq6DaNxO0CBub0j7Qd9S0wjQNHPga/opGJR+/kyItWN5H1/0nUtzt0XwGqZkv1oGk6vW eE1rELD8IhlHNc7jv6OkGIw39T6jXAiCvEBnpeU9Nr69SPnRzGuvjaZenKPBG3dc4xz4 aAtx21r12tszjyn0Zcqg5Mdbd+pgTtKejQg7Nmxa+NdasYsuEsKhtGiEKuYY7UMh2RiF T4ig== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787609784; x=1788214584; 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=agkHTfuakx1rgRKgGIKM0NPif0KsJOC9FtBiJzgivbg=; b=nysX9gIorduf+C0RQLLL3WYoH9aQNxpX2Jprr/lr3wATMr+K/8aV5Z+2mk184qYD3w dUBhoO44SVscnZl50YnuEguWGjok/9/yZEswIw2pjCwAciRgyc4bMnjgySEtbJdox6hI Pswxaovuo7vx0UV9yKOSibrHP98e5jX8ta6bxFY08WIaLklQYiSQ2WhdzMhH4E4drARt qNEtuKvyQdcIkchqhR1cwFKhQqGL2e9pE7+sivj3dEBMkI90wwDx8yhO0JT1Uf0B16Tg Hqql5AhbK+hIn3r1U5/DTEGtRPOpJKgbxvjZUH0MiyhxdoVqN30VG8MoN5nOfSR+Z4GM aIlw== X-Gm-Message-State: AFuF++mCvbGM6F7RaCgL8AK/njRyWyydrF5dqQsxDpgiU78RKHeOGoVG wO0jQ1+SGBQNPxwy7gagFXmPbz6OaQN9mvtPTc9f8IK/c7UmaIv8VgKdgczLFg== X-Gm-Gg: AR+sD11+j6b0Bkie/xlwMRleBLUA5DMn+KJ2t7XeAjluA2j48to036gYk8NLMYOul8j RIh27R0BWB2QdQbeaeCZ3+F/9YS9tvDKWkQd6vAYEEPuMyIIkST6Cytr0RpWaX8FQ9J3dn4oU6j PROMFpS5TBBJWpMTg2YiV1o2PwJ8gX/lPhPsT44O3FoBtWcvQ/a2KxWwQMIS6TaKyY0ra25MQ/h h7M50VDo8r+SEYG4QU//a0DZHLRyBin0KMgB8qzIgvc90gvAaDzsuwfgAvJL/4ix9+Gzmd0+yvm gFH88OvXhZMQt9t4ZUXWzl9a1Mrh+QZk4f8z0VPZyj9zuK7KgRJp8AHdidYuqLbexufEcj4r1Br 9cbHNk7ZrS6x74E96oprWowVMzsvq3iseXK7+PMOP9I07WEaZ6/LLV+7x6sXLpJmisqC0WHQt8k EoadBhR5dx/5MmJsKny9Zti9VsccQXvoEdnVbhAf4lK6JrcOKDQ5rcIyiP X-Received: by 2002:a53:b887:0:b0:66c:c830:c28b with SMTP id 956f58d0204a3-66d14b1c327mr591031d50.36.1787609783949; Mon, 24 Aug 2026 15:16:23 -0700 (PDT) Received: from localhost ([67.197.117.177]) by smtp.gmail.com with ESMTPSA id 956f58d0204a3-66cf4624e9esm4500581d50.2.2026.08.24.15.16.23 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 24 Aug 2026 15:16:23 -0700 (PDT) From: Matt Turner To: libc-alpha@sourceware.org Cc: Matt Turner Subject: [PATCH 3/7] stdio-common: Add printf format tests for the b and B conversions Date: Mon, 24 Aug 2026 18:16:08 -0400 Message-ID: <20260824221612.4132669-4-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.7 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 b and B conversions were left out when the formatted printf output tests were added, because gawk does not handle them at all. Verification no longer goes through AWK, so cover them now. They follow the existing integer conversions, with the alternative form producing the 0b or 0B prefix for a nonzero value. Note that B was listed for neither the '#' and '0' flags nor precision, so add it to those lists next to b, as otherwise most of its records would never be produced. Tested on x86_64-linux-gnu, where all 576 results pass. Reviewed-by: Adhemerval Zanella --- stdio-common/tst-printf-format-skeleton.c | 6 +++--- stdio-common/tst-printf-format-uchar.sh | 2 +- stdio-common/tst-printf-format-uint.sh | 2 +- stdio-common/tst-printf-format-ullong.sh | 2 +- stdio-common/tst-printf-format-ulong.sh | 2 +- stdio-common/tst-printf-format-ushort.sh | 2 +- stdio-common/tst-printf-format.py | 10 +++++----- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git ./stdio-common/tst-printf-format-skeleton.c ./stdio-common/tst-printf-format-skeleton.c index 3594124513..c880da55e2 100644 --- ./stdio-common/tst-printf-format-skeleton.c +++ ./stdio-common/tst-printf-format-skeleton.c @@ -82,9 +82,9 @@ /* The list of conversions permitted for the '#' flag, the '0' flag, and precision respectively. */ -#define HASH_FORMATS "boxXaAeEfFgG" -#define ZERO_FORMATS "bdiouxXaAeEfFgG" -#define PREC_FORMATS "bdiouxXaAeEfFgGs" +#define HASH_FORMATS "bBoxXaAeEfFgG" +#define ZERO_FORMATS "bBdiouxXaAeEfFgG" +#define PREC_FORMATS "bBdiouxXaAeEfFgGs" /* Output format conversion flags. */ static struct diff --git ./stdio-common/tst-printf-format-uchar.sh ./stdio-common/tst-printf-format-uchar.sh index d0d3627e8d..afd8de8ae6 100644 --- ./stdio-common/tst-printf-format-uchar.sh +++ ./stdio-common/tst-printf-format-uchar.sh @@ -25,7 +25,7 @@ test_program_prefix=$1; shift status=0 -for f in o u x X; do +for f in B b o u x X; do echo Verifying $f (set -o pipefail ${test_program_prefix} \ diff --git ./stdio-common/tst-printf-format-uint.sh ./stdio-common/tst-printf-format-uint.sh index 2c32133212..de4cab01d4 100644 --- ./stdio-common/tst-printf-format-uint.sh +++ ./stdio-common/tst-printf-format-uint.sh @@ -25,7 +25,7 @@ test_program_prefix=$1; shift status=0 -for f in o u x X; do +for f in B b o u x X; do echo Verifying $f (set -o pipefail ${test_program_prefix} \ diff --git ./stdio-common/tst-printf-format-ullong.sh ./stdio-common/tst-printf-format-ullong.sh index 1136f61bf9..5402ce1e0b 100644 --- ./stdio-common/tst-printf-format-ullong.sh +++ ./stdio-common/tst-printf-format-ullong.sh @@ -25,7 +25,7 @@ test_program_prefix=$1; shift status=0 -for f in o u x X; do +for f in B b o u x X; do echo Verifying $f (set -o pipefail ${test_program_prefix} \ diff --git ./stdio-common/tst-printf-format-ulong.sh ./stdio-common/tst-printf-format-ulong.sh index 15802cec63..bc4800e0e1 100644 --- ./stdio-common/tst-printf-format-ulong.sh +++ ./stdio-common/tst-printf-format-ulong.sh @@ -25,7 +25,7 @@ test_program_prefix=$1; shift status=0 -for f in o u x X; do +for f in B b o u x X; do echo Verifying $f (set -o pipefail ${test_program_prefix} \ diff --git ./stdio-common/tst-printf-format-ushort.sh ./stdio-common/tst-printf-format-ushort.sh index b08ae2906c..a11de87ecb 100644 --- ./stdio-common/tst-printf-format-ushort.sh +++ ./stdio-common/tst-printf-format-ushort.sh @@ -25,7 +25,7 @@ test_program_prefix=$1; shift status=0 -for f in o u x X; do +for f in B b o u x X; do echo Verifying $f (set -o pipefail ${test_program_prefix} \ diff --git ./stdio-common/tst-printf-format.py ./stdio-common/tst-printf-format.py index d10580ae8a..a24ea5d402 100644 --- ./stdio-common/tst-printf-format.py +++ ./stdio-common/tst-printf-format.py @@ -40,10 +40,10 @@ from fractions import Fraction # Conversions grouped by the C type of the corresponding argument. FLOAT_CONVS = frozenset("eEfFgG") -INT_CONVS = frozenset("diouxX") +INT_CONVS = frozenset("bBdiouxX") # The conversion specifier selects the base an integer is written in. -INT_FORMATS = {"o": "%o", "u": "%d", "x": "%x", "X": "%X"} +INT_FORMATS = {"b": "b", "B": "b", "o": "o", "u": "d", "x": "x", "X": "X"} # Flags and length modifiers accepted in a conversion specification. FLAG_CHARS = "-+ #0" @@ -223,7 +223,7 @@ def convert_int(value, spec): digits = str(-value if neg else value) sign = spec.sign_of(neg) else: - digits = INT_FORMATS[conv] % value + digits = format(value, INT_FORMATS[conv]) sign = "" if spec.prec is not None: # A zero value converted with a precision of zero produces no @@ -234,10 +234,10 @@ def convert_int(value, spec): if spec.alt: if conv == "o" and not digits.startswith("0"): digits = "0" + digits - elif conv in "xX" and value != 0: + elif conv in "bBxX" and value != 0: # The base prefix precedes any '0' flag padding, so it pads # along with the sign rather than with the digits. - sign = "0x" if conv == "x" else "0X" + sign = "0" + conv # An explicit precision defeats the '0' flag. return spec.pad(digits, zero_ok=spec.prec is None, sign=sign) 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": 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("%"): From patchwork Mon Aug 24 22:16:11 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Matt Turner X-Patchwork-Id: 142127 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 4D3F54BA798C for ; Mon, 24 Aug 2026 22:18:57 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 4D3F54BA798C 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=f6RrI+5a X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-yx1-xb12c.google.com (mail-yx1-xb12c.google.com [IPv6:2607:f8b0:4864:20::b12c]) by sourceware.org (Postfix) with ESMTPS id 274984BA9029 for ; Mon, 24 Aug 2026 22:16:30 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 274984BA9029 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 274984BA9029 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::b12c ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609790; cv=none; b=TVciInISjUTZniERpzRKpn3C1dYp2AuExUwBSRQFDccVQZ66rn/MOH71+F7fvVF0OnGp89nt6f31fD+xNIiwOQHR/VvspNQgNJ7R+wUrf1us71QllsowO8W9Mi9sh2d57BqsbHom4/mFlexbfXTBCpIaT7Vcl387LQJqenSjeIo= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609790; c=relaxed/simple; bh=9Q0LV0um4ezkVzgpV9c7eRdz+6X8PsQsQ6qeDpfEUrY=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=Ew/l9dLgUTSs2kV/hBa3w/e5Aykbr4TMPdNHk5kRWq89Sexqycsuy1JnJD1KLSmHE+lg+IBqmlimZ+LLcNxSD9pBr3LQqb3xtQ4vafTNLPZPmtzQlUFB8hVZRZNCEgEGBXVoK/o1e0rcEqFDm7mPo+22wre9QQzGRp/8eBA8Mrw= 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=f6RrI+5a DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 274984BA9029 Received: by mail-yx1-xb12c.google.com with SMTP id 956f58d0204a3-66c82b32121so6049818d50.1 for ; Mon, 24 Aug 2026 15:16:30 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787609789; x=1788214589; 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=7BauixMcepc6/5KutIkIqBbXUYvFwj64pQw6kNIDJtE=; b=f6RrI+5aVjkYIIVq+wBwDQE++C7AmPrC1comPB9XiGHV2C0MO+4CkISDOLz/mgy/Y2 KFoKybAiY6DL4rjeIVZXabYzRFHAy3r1NW8g92rTcELccJr+eFz0tjq/RRCemaiiy9hv bUWN+gqdXmDolwuiych8rSQ96QMAKKnM2NlFLhV7616fLxrFbTbChAtQ1/utOVFg8imT drKfczlB7MB7RlGv354MJUor/q1dhP6HiGtmtR2nlAk2wawsEiMSV4/ReqRjoV+FjIpG vvMOJaWv1ZLO+uAcbElaFSNrc1JjNFf7rujtYxAjFGMJ8JRuVuhDtp3FAA85MBgjLVXz CQEw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787609789; x=1788214589; 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=7BauixMcepc6/5KutIkIqBbXUYvFwj64pQw6kNIDJtE=; b=UBcruhhiZmGBMSwMWkZ8EYtP+v0SqFwkfFFR8hoguyBrPyHDpEM7+2+zkYz0798aI6 fNsXE6p9b8KeTPAye13klDlGsM6R3AwO3c+fv+o/Drtyf5+28/jvfmrixUs9bfASS5iO pbSYLomIQqOeJ/kG9k2PN1BAfci0yliRfZWPdhIQP9g9QOKu/eEkEohmhs6VQJ1BjkEn i3EbFt+B8gDAGFpJMuo/jRabuqairrRBkIcnTMWNMz2aECgVCjMbHqN1QzXtrcs4zm+A /0tQ660DamJ8GuCbMyPDHJAC21a4ltmodputoB9rzhzznFg/Ra73Z/QrPT6c39mnx0nE Z30g== X-Gm-Message-State: AFuF++lpmtTIDPY0Hee44qX76WnosBjlenqoM0JiexggDLgXyHe7kg5F eD8k1O5YreWOGoYzjcnfAqK7bQOrJ9YtFPPASdGxiPbhQ4/ZGyfXh3X1sfnskQ== X-Gm-Gg: AR+sD10qNqxzVeb4wymjCwKU4aFAfPc5YM7ixOs6s+ZCHAnG1vvT4EcGI+7vrH4JFWc 1thHccT9wzo1iEdpnFlGhLJHReityM62Abz8C6wi6JIwYZ0stQ5oQCeNxrd5KvIN6B2ffFrCVNo M3pfXgCr4XUACT59pP7cRU/cf1zF6Wg8r40F2a7ez2YP3fYFETUVuPjhX7qO/BUFv+W6paQM4NI bXCgYSkTEQNGnit81oECM5DjvnvDBdJYtC1UMMGMquTkRkFwKAZnnQYS6N+ca+/OFzBQfZqdQaL KnBfnjNrUuTXshNKo2JKidE7h2JJV1ZJQRn6R8NPi6+kfC2SG94VYTcV269Qn/H2Hs9hdr/lrxs uLkLwbagJ0xhCsf0D9mKoH3xfrD1pJ3Q4NB2OSHK5iDvzOAiXnSX9Hmzum9NBcLE3iiJF6q6pWJ 9mbp2rQj3iU46DWseprTTeTf5WWbvxzqxdY/qHIwN/3VU5h/QJgbZKCzk0 X-Received: by 2002:a05:690e:144c:b0:66c:f6a3:32a5 with SMTP id 956f58d0204a3-66cf6a332d6mr6611045d50.15.1787609789330; Mon, 24 Aug 2026 15:16:29 -0700 (PDT) Received: from localhost ([67.197.117.177]) by smtp.gmail.com with ESMTPSA id 00721157ae682-853ff053a84sm10032657b3.4.2026.08.24.15.16.28 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 24 Aug 2026 15:16:28 -0700 (PDT) From: Matt Turner To: libc-alpha@sourceware.org Cc: Matt Turner Subject: [PATCH 6/7] stdio-common: Keep trailing zeros where %#g rounds into a new decade Date: Mon, 24 Aug 2026 18:16:11 -0400 Message-ID: <20260824221612.4132669-7-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 g and G conversions choose between the f and e styles according to the exponent the value has once rounded to the requested number of significant digits. Where the value is small enough for the f style but rounding then carries into a new decade, printf_fp rewrites the digits it has already produced into the e style, recomputing along the way how many fractional digits the leading digit now leaves room for. FRACDIG_MIN, which the alternative form sets to the number of fractional digits that have to be retained rather than stripped, was left behind at the value computed for the f style. Where the value filled the whole integer part that is zero, so all the fractional digits were then stripped from a result the '#' flag requires to keep them: printf ("%#.2g", 99.9) gave "1.e+02" rather than "1.0e+02" printf ("%#g", 999999.9) gave "1.e+06" rather than "1.00000e+06" Update FRACDIG_MIN along with FRACDIG_MAX. Without the alternative form nothing retains trailing zeros, so the outcome is unchanged there. None of the values the conversion tests iterate over round this way, so add one that does at a precision they cover. Tested on x86_64-linux-gnu. Reviewed-by: Adhemerval Zanella --- stdio-common/printf_fp.c | 5 +++++ stdio-common/tst-printf-format-skeleton-double.c | 2 +- stdio-common/tst-printf-format-skeleton-ldouble.c | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git ./stdio-common/printf_fp.c ./stdio-common/printf_fp.c index b8f8c5fc07..97bd6afda3 100644 --- ./stdio-common/printf_fp.c +++ ./stdio-common/printf_fp.c @@ -896,6 +896,11 @@ __printf_fp_buffer_1 (struct __printf_buffer *buf, locale_t loc, fracdig_no += intdig_no; intdig_no = 1; fracdig_max = intdig_max - intdig_no; + if (info->alt) + /* The alternative form retains trailing zeros, and + the number of fractional digits it has to retain + has just changed along with FRACDIG_MAX. */ + fracdig_min = fracdig_max; ++p.exponent; /* Now we must print the p.exponent. */ p.type = isupper (info->spec) ? 'E' : 'e'; diff --git ./stdio-common/tst-printf-format-skeleton-double.c ./stdio-common/tst-printf-format-skeleton-double.c index ae3acb00f7..333bd568a2 100644 --- ./stdio-common/tst-printf-format-skeleton-double.c +++ ./stdio-common/tst-printf-format-skeleton-double.c @@ -30,7 +30,7 @@ typedef double type_t; static const type_t vals[] = { -HUGE_VAL, -DBL_MAX, -DBL_MIN, -0.0, -NAN, NAN, 0, DBL_TRUE_MIN, - DBL_MIN, DBL_MAX, HUGE_VAL }; + DBL_MIN, 99.9, 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 e323d621bb..0c828ec3b5 100644 --- ./stdio-common/tst-printf-format-skeleton-ldouble.c +++ ./stdio-common/tst-printf-format-skeleton-ldouble.c @@ -31,7 +31,7 @@ typedef long double type_t; static const type_t vals[] = { -HUGE_VAL, -LDBL_MAX, -LDBL_MIN, -0.0, -NAN, NAN, 0, LDBL_TRUE_MIN, - LDBL_MIN, LDBL_MAX, HUGE_VAL }; + LDBL_MIN, 99.9L, LDBL_MAX, HUGE_VAL }; static const char length[] = "L"; #ifndef TIMEOUT From patchwork Mon Aug 24 22:16:12 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Matt Turner X-Patchwork-Id: 142126 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 EB8554BA799B for ; Mon, 24 Aug 2026 22:17:46 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org EB8554BA799B 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=lwD65ZwL X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-yx1-xb130.google.com (mail-yx1-xb130.google.com [IPv6:2607:f8b0:4864:20::b130]) by sourceware.org (Postfix) with ESMTPS id A97864BA9001 for ; Mon, 24 Aug 2026 22:16:31 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org A97864BA9001 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 A97864BA9001 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::b130 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609791; cv=none; b=dcAGl0NS0ByOhxUlwQyZmB/jjA2DWv/pWcgukY45O3tt9TXWud4da+FMw/DDEWjtuTggpMKMAtUTm04BrwJ2etper+q2uRh91rYAheBYG0AmMUjTM0fux3FW1pLuy69LNioFb8c0Ri8rdIhkne8R1Y/VZwm4LxtYWngL4f26p2w= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1787609791; c=relaxed/simple; bh=cUgyGp3HMh2W6Opinmv5BVmaIa9J/lBCjvSYwzxiHIk=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=Oif8AaefaO2gGrKMlUXDKbuP7L+GAMc3sfMjIH3+dyEjAgyaAuaaq7wyNxAZMlaQO3BhxXT+VbNoScvoo0JseYIvQXylo8vuITW3q6lczGVeGK5ZLAkFyc1cAq8lyH0p3IovLIZyva+YEndd3FGidfMUt+Kjt9IAhgCRLR79FHo= 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=lwD65ZwL DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org A97864BA9001 Received: by mail-yx1-xb130.google.com with SMTP id 956f58d0204a3-66d146f04cfso629632d50.0 for ; Mon, 24 Aug 2026 15:16:31 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1787609791; x=1788214591; 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=u/rUDLKuB6+i6vtwhGPHWOaoa5HXknL0GUodsS9gR+8=; b=lwD65ZwLi/SCjgAaSbuoaOz/l7C5HFHNWKX0FiFvZGXWMNX9oJhxBeE2fplOQwBDQd HKSNGTY0ySbHhHpsPJt+/GRtxZTYa8ghsQ9vqHS+zy/uWoSGFnQuheAslXcDO5VERhyy AikV04oTRKBym0HORdy1oi8KSyXtiFuDhNRjogPJrzsBSK2nWYIuYlCQO8EnlJHXnIh5 qUH/FONbPZSYPfETY24SvlnPG/4A3R80KGPem73o5x3zd9dH39Z/4oIRXJuCWR+5VbBf uKzZrdoSUiBxb2D1ERXXb0Zs7qIXWY6hOlRTMx3UpbGo2cGj2w1QYBr5yxsXMU2T+YC4 jnHQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787609791; x=1788214591; 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=u/rUDLKuB6+i6vtwhGPHWOaoa5HXknL0GUodsS9gR+8=; b=FcZarBiJvTgriyniqxJMD7zkgWPOT7Dq/zrh/2wk4MI4UbvxUxl5Mtsecqm4B1tphe rIFDb2Reb2bJazECXI+Y6XWkFnIzZTwXDH4N3K05PCtaU67h9wkbwuGBO/6moGSuHaOe yUpQP0pwai+qFXiziefzAuTGDmE2twzSyvm+oyi959Yus3IDrlX5ukgUdeAEEKJ72T9H RaB8+m7qY8w6N947NEmvYxO4OD8SQpooh5sAi0TPK/uvad98tovKNi7H+08+6lNJPE/I RtWidGmwOB7FzD0MZOs6+S5jGDXcEkeBIBA9y+siq59VxmvnHf80gCxn8twPZ1/Nfmtf w8wA== X-Gm-Message-State: AFuF++lXGREFdzixslbeO7iXIhySUa52eajfT7RDfxG1NupeG+ARZ7v9 7Nj7w160o5bZKlhsHk6QRqN06O1WhWmRTJMq+43JhmNlkN8nARA/xwFkrhKWPQ== X-Gm-Gg: AR+sD135iKE28qc9BquKcnXFmYYMAhXBwIYxzYPMFS0xXcRsqSKAQ0tfSz/GKaB13wB LbuJi7Xg+SNb2LZ8mYLY6UMHXO6lDMzhiLnCldLXfZi7jdAZAs6dmamZvmrDE4zZ/GjJUcFZ4EX Yf2rFQaMbaK3+5UzKGPn3Lf+pd82zbffLP9SqW71n6B2J9ehCFqz0khdGjIw0TEA1Uid7J9hxR/ YOpQJ7JeLCw3swbKw9BqNAAidujNC2M8834tGTu5XBIR1zK7u7Y2LOtFIdSFxEHKlJ4OKOO32pQ H+6y5P0Xbc/LAru/ULNZmI6hYXbU9Hy80uW6fMjn6VucyWSoIBnKPaxWs6QLjhJ3hx9yoIljlBY rCvajUmQNfZ1J3Zp7e/ZLJ4yvNdXk27pOECaL/E73uamLkNbV8Vorgjybx6SqTbOQpYxLEYrr9K /u6ChDVkN4bMLSAHgnaKEIKaNHnYn4Q3BwuN5G87wL5FJEn1APEGUjYGgT X-Received: by 2002:a53:c087:0:b0:66c:c7ec:27f5 with SMTP id 956f58d0204a3-66ce4b8b4e3mr7949721d50.54.1787609790924; Mon, 24 Aug 2026 15:16:30 -0700 (PDT) Received: from localhost ([67.197.117.177]) by smtp.gmail.com with ESMTPSA id 00721157ae682-84cac00f57dsm40103827b3.40.2026.08.24.15.16.30 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 24 Aug 2026 15:16:30 -0700 (PDT) From: Matt Turner To: libc-alpha@sourceware.org Cc: Matt Turner Subject: [PATCH 7/7] stdio-common: Skip the a and A conversions for the IBM extended format Date: Mon, 24 Aug 2026 18:16:12 -0400 Message-ID: <20260824221612.4132669-8-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=-11.0 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 renders the a and A conversions by splitting the value into a significand of the width the type has and a power of two. That presumes the type is a single binary floating-point format. The IBM extended format is a pair of doubles instead, whose combined significand has no fixed width and whose subnormals are those of the low double rather than of the type, so what glibc produces for it does not follow from PREC and MINEXP: LDBL_MAX comes out as 0x1.fffffffffffff7ffffffffffff8p+1023 with twenty-eight fractional digits where the value has at most twenty-six worth of significand, and LDBL_TRUE_MIN as 0x0.0000000000001p-1022, which is the smallest subnormal double and nowhere near LDBL_MIN_EXP. The remaining conversions are unaffected, as they work from the value rather than from a decomposition of it, and were verified to be. Add an UNSUPPORTED_CONVS definition to the skeleton for conversions that cannot be modeled for the type at hand, and set it to the a and A pair where long double has that format, the generator then producing no records and an unsupported status which the long double wrapper reports. This does not come up in practice: powerpc64le is the one target with the format, and it builds everything with the IEEE format instead, so the test programs there see a 113 bit significand. That is done for shared builds only though, which leaves the conversions failing where shared libraries have been turned off. Tested on x86_64-linux-gnu and powerpc64le-linux-gnu, where all 672 results pass, and with the long double conversions forced to the IBM extended format, where the a and A ones report unsupported and the rest continue to pass. Reviewed-by: Adhemerval Zanella &1 | head -n 1 | sed "s/^/Conversion $format output error, first line:\n/") 2>&1 || - status=1 + rc=$? + +# The generator produces no records and reports an unsupported status for +# a conversion the verification cannot model for the long double format in +# use; see UNSUPPORTED_CONVS in tst-printf-format-skeleton-ldouble.c. +case $rc in +0) echo Verifying $format ;; +77) echo Unsupported $format; status=77 ;; +*) echo Verifying $format; status=1 ;; +esac exit $status diff --git ./stdio-common/tst-printf-format-skeleton-ldouble.c ./stdio-common/tst-printf-format-skeleton-ldouble.c index 0c828ec3b5..860b0ace7c 100644 --- ./stdio-common/tst-printf-format-skeleton-ldouble.c +++ ./stdio-common/tst-printf-format-skeleton-ldouble.c @@ -28,6 +28,15 @@ #define REF_VAL(v) (v) #define PREC LDBL_MANT_DIG #define MINEXP LDBL_MIN_EXP +#if LDBL_MANT_DIG == 106 +/* The IBM extended format is a pair of doubles rather than a significand + of a single fixed width, so what the a and A conversions produce for it + does not follow from PREC and MINEXP the way the verification assumes. + Leave them out where the type has that format; the one target concerned + builds everything with the IEEE format instead, so this only comes up + where that has been turned off. */ +# define UNSUPPORTED_CONVS "aA" +#endif typedef long double type_t; static const type_t vals[] = { -HUGE_VAL, -LDBL_MAX, -LDBL_MIN, -0.0, -NAN, NAN, 0, LDBL_TRUE_MIN, diff --git ./stdio-common/tst-printf-format-skeleton.c ./stdio-common/tst-printf-format-skeleton.c index 6250792d44..ab93e3f654 100644 --- ./stdio-common/tst-printf-format-skeleton.c +++ ./stdio-common/tst-printf-format-skeleton.c @@ -41,6 +41,10 @@ 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. + UNSUPPORTED_CONVS + [optional] String of conversions the verification cannot + model for the argument type handled. Asking for one of + these produces no records and an unsupported status. Typedefs: type_t Variadic function argument type. Define to the promoted @@ -67,6 +71,7 @@ #include #include #include +#include /* Set to nonzero to select all possible tuples with repetitions of 1..n elements from the set of flags as defined in FLAGS array below; n is @@ -87,6 +92,11 @@ #ifndef MINEXP # define MINEXP 0 #endif +/* Set to the conversions that cannot be verified for the type handled; + empty where they all can be, which is the usual case. */ +#ifndef UNSUPPORTED_CONVS +# define UNSUPPORTED_CONVS "" +#endif /* The list of conversions permitted for the '#' flag, the '0' flag, and precision respectively. */ @@ -333,6 +343,10 @@ do_test (int argc, char *argv[]) return EXIT_FAILURE; } + c = *argv[1]; + if (strchr (UNSUPPORTED_CONVS, c) != NULL) + return EXIT_UNSUPPORTED; + mtrace (); if (PREC != 0 && printf ("prec:%i\n", PREC) < 0) @@ -347,7 +361,6 @@ do_test (int argc, char *argv[]) return EXIT_FAILURE; } - c = *argv[1]; for (v = 0; v < array_length (vals); v++) { if (printf ("val:%" REF_FMT "\n", REF_VAL (vals[v])) < 0)