Remove dependence on -fwrapv semantics in a few places.
authorNathan Bossart <nathan@postgresql.org>
Thu, 15 Aug 2024 20:47:31 +0000 (15:47 -0500)
committerNathan Bossart <nathan@postgresql.org>
Thu, 15 Aug 2024 20:47:31 +0000 (15:47 -0500)
This commit attempts to update a few places, such as the money,
numeric, and timestamp types, to no longer rely on signed integer
wrapping for correctness.  This is intended to move us closer
towards removing -fwrapv, which may enable some compiler
optimizations.  However, there is presently no plan to actually
remove that compiler option in the near future.

Besides using some of the existing overflow-aware routines in
int.h, this commit introduces and makes use of some new ones.
Specifically, it adds functions that accept a signed integer and
return its absolute value as an unsigned integer with the same
width (e.g., pg_abs_s64()).  It also adds functions that accept an
unsigned integer, store the result of negating that integer in a
signed integer with the same width, and return whether the negation
overflowed (e.g., pg_neg_u64_overflow()).

Finally, this commit adds a couple of tests for timestamps near
POSTGRES_EPOCH_JDATE.

Author: Joseph Koshakow
Reviewed-by: Tom Lane, Heikki Linnakangas, Jian He
Discussion: https://postgr.es/m/CAAvxfHdBPOyEGS7s%2Bxf4iaW0-cgiq25jpYdWBqQqvLtLe_t6tw%40mail.gmail.com

src/backend/utils/adt/cash.c
src/backend/utils/adt/numeric.c
src/backend/utils/adt/numutils.c
src/backend/utils/adt/timestamp.c
src/include/common/int.h
src/interfaces/ecpg/pgtypeslib/timestamp.c
src/test/regress/expected/timestamp.out
src/test/regress/expected/timestamptz.out
src/test/regress/sql/timestamp.sql
src/test/regress/sql/timestamptz.sql

index ec3c08acfc2df970df90b15265ee57965177e131..c1a743b2a6b782ba0199b299ad6974a19f221a6b 100644 (file)
@@ -387,6 +387,7 @@ Datum
 cash_out(PG_FUNCTION_ARGS)
 {
    Cash        value = PG_GETARG_CASH(0);
+   uint64      uvalue;
    char       *result;
    char        buf[128];
    char       *bufptr;
@@ -429,8 +430,6 @@ cash_out(PG_FUNCTION_ARGS)
 
    if (value < 0)
    {
-       /* make the amount positive for digit-reconstruction loop */
-       value = -value;
        /* set up formatting data */
        signsymbol = (*lconvert->negative_sign != '\0') ? lconvert->negative_sign : "-";
        sign_posn = lconvert->n_sign_posn;
@@ -445,6 +444,9 @@ cash_out(PG_FUNCTION_ARGS)
        sep_by_space = lconvert->p_sep_by_space;
    }
 
+   /* make the amount positive for digit-reconstruction loop */
+   uvalue = pg_abs_s64(value);
+
    /* we build the digits+decimal-point+sep string right-to-left in buf[] */
    bufptr = buf + sizeof(buf) - 1;
    *bufptr = '\0';
@@ -470,10 +472,10 @@ cash_out(PG_FUNCTION_ARGS)
            memcpy(bufptr, ssymbol, strlen(ssymbol));
        }
 
-       *(--bufptr) = ((uint64) value % 10) + '0';
-       value = ((uint64) value) / 10;
+       *(--bufptr) = (uvalue % 10) + '0';
+       uvalue = uvalue / 10;
        digit_pos--;
-   } while (value || digit_pos >= 0);
+   } while (uvalue || digit_pos >= 0);
 
    /*----------
     * Now, attach currency symbol and sign symbol in the correct order.
index 77f64331f3659627d1d78b35bb734c20db640f8e..44d88e900792f8b58d336dc72158f87fcef45411 100644 (file)
@@ -8113,7 +8113,7 @@ int64_to_numericvar(int64 val, NumericVar *var)
    if (val < 0)
    {
        var->sign = NUMERIC_NEG;
-       uval = -val;
+       uval = pg_abs_s64(val);
    }
    else
    {
@@ -11584,7 +11584,7 @@ power_var_int(const NumericVar *base, int exp, int exp_dscale,
     * Now we can proceed with the multiplications.
     */
    neg = (exp < 0);
-   mask = abs(exp);
+   mask = pg_abs_s32(exp);
 
    init_var(&base_prod);
    set_var_from_var(base, &base_prod);
index adc1e8a4cba205f551cedf05ebb03571b367858b..63c2beb6a29720463dac87a524c0a9cef32ce256 100644 (file)
@@ -18,6 +18,7 @@
 #include <limits.h>
 #include <ctype.h>
 
+#include "common/int.h"
 #include "port/pg_bitutils.h"
 #include "utils/builtins.h"
 
@@ -131,6 +132,7 @@ pg_strtoint16_safe(const char *s, Node *escontext)
    uint16      tmp = 0;
    bool        neg = false;
    unsigned char digit;
+   int16       result;
 
    /*
     * The majority of cases are likely to be base-10 digits without any
@@ -190,10 +192,9 @@ pg_strtoint16_safe(const char *s, Node *escontext)
 
    if (neg)
    {
-       /* check the negative equivalent will fit without overflowing */
-       if (unlikely(tmp > (uint16) (-(PG_INT16_MIN + 1)) + 1))
+       if (unlikely(pg_neg_u16_overflow(tmp, &result)))
            goto out_of_range;
-       return -((int16) tmp);
+       return result;
    }
 
    if (unlikely(tmp > PG_INT16_MAX))
@@ -333,10 +334,9 @@ slow:
 
    if (neg)
    {
-       /* check the negative equivalent will fit without overflowing */
-       if (tmp > (uint16) (-(PG_INT16_MIN + 1)) + 1)
+       if (unlikely(pg_neg_u16_overflow(tmp, &result)))
            goto out_of_range;
-       return -((int16) tmp);
+       return result;
    }
 
    if (tmp > PG_INT16_MAX)
@@ -393,6 +393,7 @@ pg_strtoint32_safe(const char *s, Node *escontext)
    uint32      tmp = 0;
    bool        neg = false;
    unsigned char digit;
+   int32       result;
 
    /*
     * The majority of cases are likely to be base-10 digits without any
@@ -452,10 +453,9 @@ pg_strtoint32_safe(const char *s, Node *escontext)
 
    if (neg)
    {
-       /* check the negative equivalent will fit without overflowing */
-       if (unlikely(tmp > (uint32) (-(PG_INT32_MIN + 1)) + 1))
+       if (unlikely(pg_neg_u32_overflow(tmp, &result)))
            goto out_of_range;
-       return -((int32) tmp);
+       return result;
    }
 
    if (unlikely(tmp > PG_INT32_MAX))
@@ -595,10 +595,9 @@ slow:
 
    if (neg)
    {
-       /* check the negative equivalent will fit without overflowing */
-       if (tmp > (uint32) (-(PG_INT32_MIN + 1)) + 1)
+       if (unlikely(pg_neg_u32_overflow(tmp, &result)))
            goto out_of_range;
-       return -((int32) tmp);
+       return result;
    }
 
    if (tmp > PG_INT32_MAX)
@@ -655,6 +654,7 @@ pg_strtoint64_safe(const char *s, Node *escontext)
    uint64      tmp = 0;
    bool        neg = false;
    unsigned char digit;
+   int64       result;
 
    /*
     * The majority of cases are likely to be base-10 digits without any
@@ -714,10 +714,9 @@ pg_strtoint64_safe(const char *s, Node *escontext)
 
    if (neg)
    {
-       /* check the negative equivalent will fit without overflowing */
-       if (unlikely(tmp > (uint64) (-(PG_INT64_MIN + 1)) + 1))
+       if (unlikely(pg_neg_u64_overflow(tmp, &result)))
            goto out_of_range;
-       return -((int64) tmp);
+       return result;
    }
 
    if (unlikely(tmp > PG_INT64_MAX))
@@ -857,10 +856,9 @@ slow:
 
    if (neg)
    {
-       /* check the negative equivalent will fit without overflowing */
-       if (tmp > (uint64) (-(PG_INT64_MIN + 1)) + 1)
+       if (unlikely(pg_neg_u64_overflow(tmp, &result)))
            goto out_of_range;
-       return -((int64) tmp);
+       return result;
    }
 
    if (tmp > PG_INT64_MAX)
index 69fe7860ede062fc8be42e7545b35e69c3e068c4..43800addf48bc321b0e84d5a94bf31a5c362831d 100644 (file)
@@ -618,19 +618,8 @@ make_timestamp_internal(int year, int month, int day,
    time = (((hour * MINS_PER_HOUR + min) * SECS_PER_MINUTE)
            * USECS_PER_SEC) + (int64) rint(sec * USECS_PER_SEC);
 
-   result = date * USECS_PER_DAY + time;
-   /* check for major overflow */
-   if ((result - time) / USECS_PER_DAY != date)
-       ereport(ERROR,
-               (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
-                errmsg("timestamp out of range: %d-%02d-%02d %d:%02d:%02g",
-                       year, month, day,
-                       hour, min, sec)));
-
-   /* check for just-barely overflow (okay except time-of-day wraps) */
-   /* caution: we want to allow 1999-12-31 24:00:00 */
-   if ((result < 0 && date > 0) ||
-       (result > 0 && date < -1))
+   if (unlikely(pg_mul_s64_overflow(date, USECS_PER_DAY, &result) ||
+                pg_add_s64_overflow(result, time, &result)))
        ereport(ERROR,
                (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
                 errmsg("timestamp out of range: %d-%02d-%02d %d:%02d:%02g",
@@ -2010,17 +1999,8 @@ tm2timestamp(struct pg_tm *tm, fsec_t fsec, int *tzp, Timestamp *result)
    date = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) - POSTGRES_EPOCH_JDATE;
    time = time2t(tm->tm_hour, tm->tm_min, tm->tm_sec, fsec);
 
-   *result = date * USECS_PER_DAY + time;
-   /* check for major overflow */
-   if ((*result - time) / USECS_PER_DAY != date)
-   {
-       *result = 0;            /* keep compiler quiet */
-       return -1;
-   }
-   /* check for just-barely overflow (okay except time-of-day wraps) */
-   /* caution: we want to allow 1999-12-31 24:00:00 */
-   if ((*result < 0 && date > 0) ||
-       (*result > 0 && date < -1))
+   if (unlikely(pg_mul_s64_overflow(date, USECS_PER_DAY, result) ||
+                pg_add_s64_overflow(*result, time, result)))
    {
        *result = 0;            /* keep compiler quiet */
        return -1;
index 7fc046e78afba953522d3781636eb4d04a49e194..3b1590d676f511a06fc5ea7c8adda67071dbef6e 100644 (file)
 
 /*---------
  * The following guidelines apply to all the overflow routines:
- * - If a + b overflows, return true, otherwise store the result of a + b
- * into *result. The content of *result is implementation defined in case of
- * overflow.
- * - If a - b overflows, return true, otherwise store the result of a - b
- * into *result. The content of *result is implementation defined in case of
- * overflow.
- * - If a * b overflows, return true, otherwise store the result of a * b
- * into *result. The content of *result is implementation defined in case of
+ *
+ * If the result overflows, return true, otherwise store the result into
+ * *result.  The content of *result is implementation defined in case of
  * overflow.
+ *
+ *  bool pg_add_*_overflow(a, b, *result)
+ *
+ *    Calculate a + b
+ *
+ *  bool pg_sub_*_overflow(a, b, *result)
+ *
+ *    Calculate a - b
+ *
+ *  bool pg_mul_*_overflow(a, b, *result)
+ *
+ *    Calculate a * b
+ *
+ *  bool pg_neg_*_overflow(a, *result)
+ *
+ *    Calculate -a
+ *
+ *
+ * In addition, this file contains:
+ *
+ *  <unsigned int type> pg_abs_*(<signed int type> a)
+ *
+ *    Calculate absolute value of a.  Unlike the standard library abs()
+ *    and labs() functions, the return type is unsigned, so the operation
+ *    cannot overflow.
  *---------
  */
 
@@ -97,6 +117,17 @@ pg_mul_s16_overflow(int16 a, int16 b, int16 *result)
 #endif
 }
 
+static inline uint16
+pg_abs_s16(int16 a)
+{
+   /*
+    * This first widens the argument from int16 to int32 for use with abs().
+    * The result is then narrowed from int32 to uint16.  This prevents any
+    * possibility of overflow.
+    */
+   return (uint16) abs((int32) a);
+}
+
 /*
  * INT32
  */
@@ -154,6 +185,17 @@ pg_mul_s32_overflow(int32 a, int32 b, int32 *result)
 #endif
 }
 
+static inline uint32
+pg_abs_s32(int32 a)
+{
+   /*
+    * This first widens the argument from int32 to int64 for use with
+    * i64abs().  The result is then narrowed from int64 to uint32.  This
+    * prevents any possibility of overflow.
+    */
+   return (uint32) i64abs((int64) a);
+}
+
 /*
  * INT64
  */
@@ -258,6 +300,14 @@ pg_mul_s64_overflow(int64 a, int64 b, int64 *result)
 #endif
 }
 
+static inline uint64
+pg_abs_s64(int64 a)
+{
+   if (unlikely(a == PG_INT64_MIN))
+       return (uint64) PG_INT64_MAX + 1;
+   return (uint64) i64abs(a);
+}
+
 /*------------------------------------------------------------------------
  * Overflow routines for unsigned integers
  *------------------------------------------------------------------------
@@ -318,6 +368,24 @@ pg_mul_u16_overflow(uint16 a, uint16 b, uint16 *result)
 #endif
 }
 
+static inline bool
+pg_neg_u16_overflow(uint16 a, int16 *result)
+{
+#if defined(HAVE__BUILTIN_OP_OVERFLOW)
+   return __builtin_sub_overflow(0, a, result);
+#else
+   int32       res = -((int32) a);
+
+   if (unlikely(res < PG_INT16_MIN))
+   {
+       *result = 0x5EED;       /* to avoid spurious warnings */
+       return true;
+   }
+   *result = res;
+   return false;
+#endif
+}
+
 /*
  * INT32
  */
@@ -373,6 +441,24 @@ pg_mul_u32_overflow(uint32 a, uint32 b, uint32 *result)
 #endif
 }
 
+static inline bool
+pg_neg_u32_overflow(uint32 a, int32 *result)
+{
+#if defined(HAVE__BUILTIN_OP_OVERFLOW)
+   return __builtin_sub_overflow(0, a, result);
+#else
+   int64       res = -((int64) a);
+
+   if (unlikely(res < PG_INT32_MIN))
+   {
+       *result = 0x5EED;       /* to avoid spurious warnings */
+       return true;
+   }
+   *result = res;
+   return false;
+#endif
+}
+
 /*
  * UINT64
  */
@@ -438,6 +524,35 @@ pg_mul_u64_overflow(uint64 a, uint64 b, uint64 *result)
 #endif
 }
 
+static inline bool
+pg_neg_u64_overflow(uint64 a, int64 *result)
+{
+#if defined(HAVE__BUILTIN_OP_OVERFLOW)
+   return __builtin_sub_overflow(0, a, result);
+#elif defined(HAVE_INT128)
+   int128      res = -((int128) a);
+
+   if (unlikely(res < PG_INT64_MIN))
+   {
+       *result = 0x5EED;       /* to avoid spurious warnings */
+       return true;
+   }
+   *result = res;
+   return false;
+#else
+   if (unlikely(a > (uint64) PG_INT64_MAX + 1))
+   {
+       *result = 0x5EED;       /* to avoid spurious warnings */
+       return true;
+   }
+   if (unlikely(a == (uint64) PG_INT64_MAX + 1))
+       *result = PG_INT64_MIN;
+   else
+       *result = -((int64) a);
+   return false;
+#endif
+}
+
 /*------------------------------------------------------------------------
  *
  * Comparison routines for integer types.
index f1b143fbd2e4e1e8b6adf3d2e377e06aa3b879d8..402fae6da679113bdd1cc91aa66b09d91033f608 100644 (file)
@@ -11,6 +11,7 @@
 #error -ffast-math is known to break this code
 #endif
 
+#include "common/int.h"
 #include "dt.h"
 #include "pgtypes_date.h"
 #include "pgtypes_timestamp.h"
@@ -48,14 +49,8 @@ tm2timestamp(struct tm *tm, fsec_t fsec, int *tzp, timestamp * result)
 
    dDate = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) - date2j(2000, 1, 1);
    time = time2t(tm->tm_hour, tm->tm_min, tm->tm_sec, fsec);
-   *result = (dDate * USECS_PER_DAY) + time;
-   /* check for major overflow */
-   if ((*result - time) / USECS_PER_DAY != dDate)
-       return -1;
-   /* check for just-barely overflow (okay except time-of-day wraps) */
-   /* caution: we want to allow 1999-12-31 24:00:00 */
-   if ((*result < 0 && dDate > 0) ||
-       (*result > 0 && dDate < -1))
+   if (unlikely(pg_mul_s64_overflow(dDate, USECS_PER_DAY, result) ||
+                pg_add_s64_overflow(*result, time, result)))
        return -1;
    if (tzp != NULL)
        *result = dt2local(*result, -(*tzp));
index cf337da517ef274e838820484521491018d66c75..e287260051cecc4b5796ba27feb9dfaf70310482 100644 (file)
@@ -2201,3 +2201,16 @@ select age(timestamp '-infinity', timestamp 'infinity');
 
 select age(timestamp '-infinity', timestamp '-infinity');
 ERROR:  interval out of range
+-- test timestamp near POSTGRES_EPOCH_JDATE
+select timestamp '1999-12-31 24:00:00';
+        timestamp         
+--------------------------
+ Sat Jan 01 00:00:00 2000
+(1 row)
+
+select make_timestamp(1999, 12, 31, 24, 0, 0);
+      make_timestamp      
+--------------------------
+ Sat Jan 01 00:00:00 2000
+(1 row)
+
index bfb3825ff6cf0f99288e7ef549f9f3a08f73ca3c..d01d174983e4e35387164a713a2e291d53b1a181 100644 (file)
@@ -3286,3 +3286,16 @@ SELECT age(timestamptz '-infinity', timestamptz 'infinity');
 
 SELECT age(timestamptz '-infinity', timestamptz '-infinity');
 ERROR:  interval out of range
+-- test timestamp near POSTGRES_EPOCH_JDATE
+select timestamptz '1999-12-31 24:00:00';
+         timestamptz          
+------------------------------
+ Sat Jan 01 00:00:00 2000 PST
+(1 row)
+
+select make_timestamptz(1999, 12, 31, 24, 0, 0);
+       make_timestamptz       
+------------------------------
+ Sat Jan 01 00:00:00 2000 PST
+(1 row)
+
index 820ef7752ac7cb569465a86a7c10950fc9f64d21..748469576d838525f0338cacfb0fe4fc454cf110 100644 (file)
@@ -424,3 +424,7 @@ select age(timestamp 'infinity', timestamp 'infinity');
 select age(timestamp 'infinity', timestamp '-infinity');
 select age(timestamp '-infinity', timestamp 'infinity');
 select age(timestamp '-infinity', timestamp '-infinity');
+
+-- test timestamp near POSTGRES_EPOCH_JDATE
+select timestamp '1999-12-31 24:00:00';
+select make_timestamp(1999, 12, 31, 24, 0, 0);
index ccfd90d6467ae985006e4ed19d39a6a6be44c82d..c71d5489b453b0a0665d024b939532a524cacb73 100644 (file)
@@ -668,3 +668,7 @@ SELECT age(timestamptz 'infinity', timestamptz 'infinity');
 SELECT age(timestamptz 'infinity', timestamptz '-infinity');
 SELECT age(timestamptz '-infinity', timestamptz 'infinity');
 SELECT age(timestamptz '-infinity', timestamptz '-infinity');
+
+-- test timestamp near POSTGRES_EPOCH_JDATE
+select timestamptz '1999-12-31 24:00:00';
+select make_timestamptz(1999, 12, 31, 24, 0, 0);