As discussed in the Ryu thread, herewith a draft of a patch to use strtof() for float4 input (rather than using strtod() with its double-rounding issue).
An exhaustive search shows that this does not change the resulting bit-pattern for any input string that could have been generated by PG with extra_float_digits=3 set. The risk is that values generated by other software, especially code that uses shortest-exact float output (as a number of languages seem to do, and which PG will do if the Ryu patch goes in) will be incorrectly input; though it appears that only one value (7.038531e-26) is both a possible shortest-exact representation and a rounding error (though a number of other values round incorrectly, they are not shortest representations). This includes a fallback to use strtod() the old way if the platform lacks strtof(). A variant file for the new regression tests is needed for such platforms; I've taken a stab at setting this up for the one platform we know will need it (if there are others, the buildfarm will let us know in due course). -- Andrew (irc:RhodiumToad)
diff --git a/configure b/configure index 06fc3c6835..e3176e24e9 100755 --- a/configure +++ b/configure @@ -15802,6 +15802,19 @@ esac fi +ac_fn_c_check_func "$LINENO" "strtof" "ac_cv_func_strtof" +if test "x$ac_cv_func_strtof" = xyes; then : + $as_echo "#define HAVE_STRTOF 1" >>confdefs.h + +else + case " $LIBOBJS " in + *" strtof.$ac_objext "* ) ;; + *) LIBOBJS="$LIBOBJS strtof.$ac_objext" + ;; +esac + +fi + case $host_os in diff --git a/configure.in b/configure.in index 4efb912c4d..bdaab717d7 100644 --- a/configure.in +++ b/configure.in @@ -1703,6 +1703,7 @@ AC_REPLACE_FUNCS(m4_normalize([ strlcat strlcpy strnlen + strtof ])) case $host_os in diff --git a/src/backend/utils/adt/float.c b/src/backend/utils/adt/float.c index 117ded8d1d..dce6ea31cf 100644 --- a/src/backend/utils/adt/float.c +++ b/src/backend/utils/adt/float.c @@ -104,13 +104,39 @@ is_infinite(double val) /* * float4in - converts "num" to float4 + * + * Note that this code now uses strtof(), where it used to use strtod(). + * + * The motivation for using strtof() is to avoid a double-rounding problem: + * for certain decimal inputs, if you round the input correctly to a double, + * and then round the double to a float, the result is incorrect in that it + * does not match the result of rounding the decimal value to float directly. + * + * One of the best examples is 7.038531e-26: + * + * 0xAE43FDp-107 = 7.03853069185120912085...e-26 + * midpoint 7.03853100000000022281...e-26 + * 0xAE43FEp-107 = 7.03853130814879132477...e-26 + * + * making 0xAE43FDp-107 the correct float result, but if you do the conversion + * via a double, you get + * + * 0xAE43FD.7FFFFFF8p-107 = 7.03853099999999907487...e-26 + * midpoint 7.03853099999999964884...e-26 + * 0xAE43FD.80000000p-107 = 7.03853100000000022281...e-26 + * 0xAE43FD.80000008p-107 = 7.03853100000000137076...e-26 + * + * so the value rounds to the double exactly on the midpoint between the two + * nearest floats, and then rounding again to a float gives the incorrect + * result of 0xAE43FEp-107. + * */ Datum float4in(PG_FUNCTION_ARGS) { char *num = PG_GETARG_CSTRING(0); char *orig_num; - double val; + float val; char *endptr; /* @@ -135,7 +161,7 @@ float4in(PG_FUNCTION_ARGS) "real", orig_num))); errno = 0; - val = strtod(num, &endptr); + val = strtof(num, &endptr); /* did we not see anything that looks like a double? */ if (endptr == num || errno != 0) @@ -143,14 +169,14 @@ float4in(PG_FUNCTION_ARGS) int save_errno = errno; /* - * C99 requires that strtod() accept NaN, [+-]Infinity, and [+-]Inf, + * C99 requires that strtof() accept NaN, [+-]Infinity, and [+-]Inf, * but not all platforms support all of these (and some accept them * but set ERANGE anyway...) Therefore, we check for these inputs - * ourselves if strtod() fails. + * ourselves if strtof() fails. * * Note: C99 also requires hexadecimal input as well as some extended * forms of NaN, but we consider these forms unportable and don't try - * to support them. You can use 'em if your strtod() takes 'em. + * to support them. You can use 'em if your strtof() takes 'em. */ if (pg_strncasecmp(num, "NaN", 3) == 0) { @@ -196,7 +222,7 @@ float4in(PG_FUNCTION_ARGS) * detect whether it's a "real" out-of-range condition by checking * to see if the result is zero or huge. */ - if (val == 0.0 || val >= HUGE_VAL || val <= -HUGE_VAL) + if (val == 0.0 || val >= HUGE_VALF || val <= -HUGE_VALF) ereport(ERROR, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("\"%s\" is out of range for type real", @@ -232,13 +258,7 @@ float4in(PG_FUNCTION_ARGS) errmsg("invalid input syntax for type %s: \"%s\"", "real", orig_num))); - /* - * if we get here, we have a legal double, still need to check to see if - * it's a legal float4 - */ - check_float4_val((float4) val, isinf(val), val == 0); - - PG_RETURN_FLOAT4((float4) val); + PG_RETURN_FLOAT4(val); } /* diff --git a/src/include/port.h b/src/include/port.h index a55c473262..a6950c1526 100644 --- a/src/include/port.h +++ b/src/include/port.h @@ -381,6 +381,10 @@ extern int isinf(double x); #endif /* __clang__ && !__cplusplus */ #endif /* !HAVE_ISINF */ +#ifndef HAVE_STRTOF +extern float strtof(const char *nptr, char **endptr); +#endif + #ifndef HAVE_MKDTEMP extern char *mkdtemp(char *path); #endif diff --git a/src/include/utils/float.h b/src/include/utils/float.h index 0f82a25ede..5e11ab4aa9 100644 --- a/src/include/utils/float.h +++ b/src/include/utils/float.h @@ -17,6 +17,11 @@ #include <math.h> +/* this might not be needed */ +#ifndef HUGE_VALF +#define HUGE_VALF ((float)HUGE_VAL) +#endif + #ifndef M_PI /* From my RH5.2 gcc math.h file - thomas 2000-04-03 */ #define M_PI 3.14159265358979323846 diff --git a/src/port/strtof.c b/src/port/strtof.c new file mode 100644 index 0000000000..75a41fbcfa --- /dev/null +++ b/src/port/strtof.c @@ -0,0 +1,52 @@ +/*------------------------------------------------------------------------- + * + * strtof.c + * + * Portions Copyright (c) 2019, PostgreSQL Global Development Group + * + * + * IDENTIFICATION + * src/port/strtof.c + * + *------------------------------------------------------------------------- + */ + +#include "c.h" + +#include <float.h> +#include <math.h> + +/* + * strtof() is part of C99; this file is only for the benefit of obsolete + * platforms. As such, it is known to return incorrect values for edge cases, + * which have to be allowed for in variant files for regression test results + * for any such platform. + */ + +float +strtof(const char *nptr, char **endptr) +{ + int caller_errno = errno; + double dresult; + float fresult; + + errno = 0; + dresult = strtod(nptr, endptr); + fresult = (float) dresult; + + if (errno == 0) + { + /* + * Value might be in-range for double but not float. + */ + if (dresult != 0 && fresult == 0) + caller_errno = ERANGE; /* underflow */ + if (!isinf(dresult) && isinf(fresult)) + caller_errno = ERANGE; /* overflow */ + } + else + caller_errno = errno; + + errno = caller_errno; + return fresult; +} diff --git a/src/test/regress/expected/float4-misrounded-input.out b/src/test/regress/expected/float4-misrounded-input.out new file mode 100644 index 0000000000..b8795c6fdf --- /dev/null +++ b/src/test/regress/expected/float4-misrounded-input.out @@ -0,0 +1,401 @@ +-- +-- FLOAT4 +-- +CREATE TABLE FLOAT4_TBL (f1 float4); +INSERT INTO FLOAT4_TBL(f1) VALUES (' 0.0'); +INSERT INTO FLOAT4_TBL(f1) VALUES ('1004.30 '); +INSERT INTO FLOAT4_TBL(f1) VALUES (' -34.84 '); +INSERT INTO FLOAT4_TBL(f1) VALUES ('1.2345678901234e+20'); +INSERT INTO FLOAT4_TBL(f1) VALUES ('1.2345678901234e-20'); +-- test for over and under flow +INSERT INTO FLOAT4_TBL(f1) VALUES ('10e70'); +ERROR: "10e70" is out of range for type real +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('10e70'); + ^ +INSERT INTO FLOAT4_TBL(f1) VALUES ('-10e70'); +ERROR: "-10e70" is out of range for type real +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('-10e70'); + ^ +INSERT INTO FLOAT4_TBL(f1) VALUES ('10e-70'); +ERROR: "10e-70" is out of range for type real +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('10e-70'); + ^ +INSERT INTO FLOAT4_TBL(f1) VALUES ('-10e-70'); +ERROR: "-10e-70" is out of range for type real +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('-10e-70'); + ^ +-- bad input +INSERT INTO FLOAT4_TBL(f1) VALUES (''); +ERROR: invalid input syntax for type real: "" +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES (''); + ^ +INSERT INTO FLOAT4_TBL(f1) VALUES (' '); +ERROR: invalid input syntax for type real: " " +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES (' '); + ^ +INSERT INTO FLOAT4_TBL(f1) VALUES ('xyz'); +ERROR: invalid input syntax for type real: "xyz" +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('xyz'); + ^ +INSERT INTO FLOAT4_TBL(f1) VALUES ('5.0.0'); +ERROR: invalid input syntax for type real: "5.0.0" +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('5.0.0'); + ^ +INSERT INTO FLOAT4_TBL(f1) VALUES ('5 . 0'); +ERROR: invalid input syntax for type real: "5 . 0" +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('5 . 0'); + ^ +INSERT INTO FLOAT4_TBL(f1) VALUES ('5. 0'); +ERROR: invalid input syntax for type real: "5. 0" +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('5. 0'); + ^ +INSERT INTO FLOAT4_TBL(f1) VALUES (' - 3.0'); +ERROR: invalid input syntax for type real: " - 3.0" +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES (' - 3.0'); + ^ +INSERT INTO FLOAT4_TBL(f1) VALUES ('123 5'); +ERROR: invalid input syntax for type real: "123 5" +LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('123 5'); + ^ +-- special inputs +SELECT 'NaN'::float4; + float4 +-------- + NaN +(1 row) + +SELECT 'nan'::float4; + float4 +-------- + NaN +(1 row) + +SELECT ' NAN '::float4; + float4 +-------- + NaN +(1 row) + +SELECT 'infinity'::float4; + float4 +---------- + Infinity +(1 row) + +SELECT ' -INFINiTY '::float4; + float4 +----------- + -Infinity +(1 row) + +-- bad special inputs +SELECT 'N A N'::float4; +ERROR: invalid input syntax for type real: "N A N" +LINE 1: SELECT 'N A N'::float4; + ^ +SELECT 'NaN x'::float4; +ERROR: invalid input syntax for type real: "NaN x" +LINE 1: SELECT 'NaN x'::float4; + ^ +SELECT ' INFINITY x'::float4; +ERROR: invalid input syntax for type real: " INFINITY x" +LINE 1: SELECT ' INFINITY x'::float4; + ^ +SELECT 'Infinity'::float4 + 100.0; + ?column? +---------- + Infinity +(1 row) + +SELECT 'Infinity'::float4 / 'Infinity'::float4; + ?column? +---------- + NaN +(1 row) + +SELECT 'nan'::float4 / 'nan'::float4; + ?column? +---------- + NaN +(1 row) + +SELECT 'nan'::numeric::float4; + float4 +-------- + NaN +(1 row) + +SELECT '' AS five, * FROM FLOAT4_TBL; + five | f1 +------+------------- + | 0 + | 1004.3 + | -34.84 + | 1.23457e+20 + | 1.23457e-20 +(5 rows) + +SELECT '' AS four, f.* FROM FLOAT4_TBL f WHERE f.f1 <> '1004.3'; + four | f1 +------+------------- + | 0 + | -34.84 + | 1.23457e+20 + | 1.23457e-20 +(4 rows) + +SELECT '' AS one, f.* FROM FLOAT4_TBL f WHERE f.f1 = '1004.3'; + one | f1 +-----+-------- + | 1004.3 +(1 row) + +SELECT '' AS three, f.* FROM FLOAT4_TBL f WHERE '1004.3' > f.f1; + three | f1 +-------+------------- + | 0 + | -34.84 + | 1.23457e-20 +(3 rows) + +SELECT '' AS three, f.* FROM FLOAT4_TBL f WHERE f.f1 < '1004.3'; + three | f1 +-------+------------- + | 0 + | -34.84 + | 1.23457e-20 +(3 rows) + +SELECT '' AS four, f.* FROM FLOAT4_TBL f WHERE '1004.3' >= f.f1; + four | f1 +------+------------- + | 0 + | 1004.3 + | -34.84 + | 1.23457e-20 +(4 rows) + +SELECT '' AS four, f.* FROM FLOAT4_TBL f WHERE f.f1 <= '1004.3'; + four | f1 +------+------------- + | 0 + | 1004.3 + | -34.84 + | 1.23457e-20 +(4 rows) + +SELECT '' AS three, f.f1, f.f1 * '-10' AS x FROM FLOAT4_TBL f + WHERE f.f1 > '0.0'; + three | f1 | x +-------+-------------+-------------- + | 1004.3 | -10043 + | 1.23457e+20 | -1.23457e+21 + | 1.23457e-20 | -1.23457e-19 +(3 rows) + +SELECT '' AS three, f.f1, f.f1 + '-10' AS x FROM FLOAT4_TBL f + WHERE f.f1 > '0.0'; + three | f1 | x +-------+-------------+------------- + | 1004.3 | 994.3 + | 1.23457e+20 | 1.23457e+20 + | 1.23457e-20 | -10 +(3 rows) + +SELECT '' AS three, f.f1, f.f1 / '-10' AS x FROM FLOAT4_TBL f + WHERE f.f1 > '0.0'; + three | f1 | x +-------+-------------+-------------- + | 1004.3 | -100.43 + | 1.23457e+20 | -1.23457e+19 + | 1.23457e-20 | -1.23457e-21 +(3 rows) + +SELECT '' AS three, f.f1, f.f1 - '-10' AS x FROM FLOAT4_TBL f + WHERE f.f1 > '0.0'; + three | f1 | x +-------+-------------+------------- + | 1004.3 | 1014.3 + | 1.23457e+20 | 1.23457e+20 + | 1.23457e-20 | 10 +(3 rows) + +-- test divide by zero +SELECT '' AS bad, f.f1 / '0.0' from FLOAT4_TBL f; +ERROR: division by zero +SELECT '' AS five, * FROM FLOAT4_TBL; + five | f1 +------+------------- + | 0 + | 1004.3 + | -34.84 + | 1.23457e+20 + | 1.23457e-20 +(5 rows) + +-- test the unary float4abs operator +SELECT '' AS five, f.f1, @f.f1 AS abs_f1 FROM FLOAT4_TBL f; + five | f1 | abs_f1 +------+-------------+------------- + | 0 | 0 + | 1004.3 | 1004.3 + | -34.84 | 34.84 + | 1.23457e+20 | 1.23457e+20 + | 1.23457e-20 | 1.23457e-20 +(5 rows) + +UPDATE FLOAT4_TBL + SET f1 = FLOAT4_TBL.f1 * '-1' + WHERE FLOAT4_TBL.f1 > '0.0'; +SELECT '' AS five, * FROM FLOAT4_TBL; + five | f1 +------+-------------- + | 0 + | -34.84 + | -1004.3 + | -1.23457e+20 + | -1.23457e-20 +(5 rows) + +-- test edge-case coercions to integer +SELECT '32767.4'::float4::int2; + int2 +------- + 32767 +(1 row) + +SELECT '32767.6'::float4::int2; +ERROR: smallint out of range +SELECT '-32768.4'::float4::int2; + int2 +-------- + -32768 +(1 row) + +SELECT '-32768.6'::float4::int2; +ERROR: smallint out of range +SELECT '2147483520'::float4::int4; + int4 +------------ + 2147483520 +(1 row) + +SELECT '2147483647'::float4::int4; +ERROR: integer out of range +SELECT '-2147483648.5'::float4::int4; + int4 +------------- + -2147483648 +(1 row) + +SELECT '-2147483900'::float4::int4; +ERROR: integer out of range +SELECT '9223369837831520256'::float4::int8; + int8 +--------------------- + 9223369837831520256 +(1 row) + +SELECT '9223372036854775807'::float4::int8; +ERROR: bigint out of range +SELECT '-9223372036854775808.5'::float4::int8; + int8 +---------------------- + -9223372036854775808 +(1 row) + +SELECT '-9223380000000000000'::float4::int8; +ERROR: bigint out of range +-- Test for correct input rounding in edge cases. +-- These lists are from Paxson 1991, excluding subnormals and +-- inputs of over 9 sig. digits. +SELECT float4send('5e-20'::float4); + float4send +------------ + \x1f6c1e4a +(1 row) + +SELECT float4send('67e14'::float4); + float4send +------------ + \x59be6cea +(1 row) + +SELECT float4send('985e15'::float4); + float4send +------------ + \x5d5ab6c4 +(1 row) + +SELECT float4send('55895e-16'::float4); + float4send +------------ + \x2cc4a9bd +(1 row) + +SELECT float4send('7038531e-32'::float4); + float4send +------------ + \x15ae43fe +(1 row) + +SELECT float4send('702990899e-20'::float4); + float4send +------------ + \x2cf757ca +(1 row) + +SELECT float4send('3e-23'::float4); + float4send +------------ + \x1a111234 +(1 row) + +SELECT float4send('57e18'::float4); + float4send +------------ + \x6045c22c +(1 row) + +SELECT float4send('789e-35'::float4); + float4send +------------ + \x0a23de70 +(1 row) + +SELECT float4send('2539e-18'::float4); + float4send +------------ + \x2736f449 +(1 row) + +SELECT float4send('76173e28'::float4); + float4send +------------ + \x7616398a +(1 row) + +SELECT float4send('887745e-11'::float4); + float4send +------------ + \x3714f05c +(1 row) + +SELECT float4send('5382571e-37'::float4); + float4send +------------ + \x0d2eaca7 +(1 row) + +SELECT float4send('82381273e-35'::float4); + float4send +------------ + \x128289d0 +(1 row) + +SELECT float4send('750486563e-38'::float4); + float4send +------------ + \x0f18377e +(1 row) + diff --git a/src/test/regress/expected/float4.out b/src/test/regress/expected/float4.out index 2f47e1c202..3aa7f257d5 100644 --- a/src/test/regress/expected/float4.out +++ b/src/test/regress/expected/float4.out @@ -9,19 +9,19 @@ INSERT INTO FLOAT4_TBL(f1) VALUES ('1.2345678901234e+20'); INSERT INTO FLOAT4_TBL(f1) VALUES ('1.2345678901234e-20'); -- test for over and under flow INSERT INTO FLOAT4_TBL(f1) VALUES ('10e70'); -ERROR: value out of range: overflow +ERROR: "10e70" is out of range for type real LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('10e70'); ^ INSERT INTO FLOAT4_TBL(f1) VALUES ('-10e70'); -ERROR: value out of range: overflow +ERROR: "-10e70" is out of range for type real LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('-10e70'); ^ INSERT INTO FLOAT4_TBL(f1) VALUES ('10e-70'); -ERROR: value out of range: underflow +ERROR: "10e-70" is out of range for type real LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('10e-70'); ^ INSERT INTO FLOAT4_TBL(f1) VALUES ('-10e-70'); -ERROR: value out of range: underflow +ERROR: "-10e-70" is out of range for type real LINE 1: INSERT INTO FLOAT4_TBL(f1) VALUES ('-10e-70'); ^ -- bad input @@ -306,3 +306,96 @@ SELECT '-9223372036854775808.5'::float4::int8; SELECT '-9223380000000000000'::float4::int8; ERROR: bigint out of range +-- Test for correct input rounding in edge cases. +-- These lists are from Paxson 1991, excluding subnormals and +-- inputs of over 9 sig. digits. +SELECT float4send('5e-20'::float4); + float4send +------------ + \x1f6c1e4a +(1 row) + +SELECT float4send('67e14'::float4); + float4send +------------ + \x59be6cea +(1 row) + +SELECT float4send('985e15'::float4); + float4send +------------ + \x5d5ab6c4 +(1 row) + +SELECT float4send('55895e-16'::float4); + float4send +------------ + \x2cc4a9bd +(1 row) + +SELECT float4send('7038531e-32'::float4); + float4send +------------ + \x15ae43fd +(1 row) + +SELECT float4send('702990899e-20'::float4); + float4send +------------ + \x2cf757ca +(1 row) + +SELECT float4send('3e-23'::float4); + float4send +------------ + \x1a111234 +(1 row) + +SELECT float4send('57e18'::float4); + float4send +------------ + \x6045c22c +(1 row) + +SELECT float4send('789e-35'::float4); + float4send +------------ + \x0a23de70 +(1 row) + +SELECT float4send('2539e-18'::float4); + float4send +------------ + \x2736f449 +(1 row) + +SELECT float4send('76173e28'::float4); + float4send +------------ + \x7616398a +(1 row) + +SELECT float4send('887745e-11'::float4); + float4send +------------ + \x3714f05c +(1 row) + +SELECT float4send('5382571e-37'::float4); + float4send +------------ + \x0d2eaca7 +(1 row) + +SELECT float4send('82381273e-35'::float4); + float4send +------------ + \x128289d1 +(1 row) + +SELECT float4send('750486563e-38'::float4); + float4send +------------ + \x0f18377e +(1 row) + diff --git a/src/test/regress/resultmap b/src/test/regress/resultmap index 46ca5639c2..3bd1585a35 100644 --- a/src/test/regress/resultmap +++ b/src/test/regress/resultmap @@ -3,3 +3,4 @@ float8:out:i.86-.*-openbsd=float8-small-is-zero.out float8:out:i.86-.*-netbsd=float8-small-is-zero.out float8:out:m68k-.*-netbsd=float8-small-is-zero.out float8:out:i.86-pc-cygwin=float8-small-is-zero.out +float4:out:hppa.*-hp-hpux10.*=float4-misrounded-input.out diff --git a/src/test/regress/sql/float4.sql b/src/test/regress/sql/float4.sql index 46a9166d13..12421f7c75 100644 --- a/src/test/regress/sql/float4.sql +++ b/src/test/regress/sql/float4.sql @@ -95,3 +95,24 @@ SELECT '9223369837831520256'::float4::int8; SELECT '9223372036854775807'::float4::int8; SELECT '-9223372036854775808.5'::float4::int8; SELECT '-9223380000000000000'::float4::int8; + +-- Test for correct input rounding in edge cases. +-- These lists are from Paxson 1991, excluding subnormals and +-- inputs of over 9 sig. digits. + +SELECT float4send('5e-20'::float4); +SELECT float4send('67e14'::float4); +SELECT float4send('985e15'::float4); +SELECT float4send('55895e-16'::float4); +SELECT float4send('7038531e-32'::float4); +SELECT float4send('702990899e-20'::float4); + +SELECT float4send('3e-23'::float4); +SELECT float4send('57e18'::float4); +SELECT float4send('789e-35'::float4); +SELECT float4send('2539e-18'::float4); +SELECT float4send('76173e28'::float4); +SELECT float4send('887745e-11'::float4); +SELECT float4send('5382571e-37'::float4); +SELECT float4send('82381273e-35'::float4); +SELECT float4send('750486563e-38'::float4);