diff --git a/src/integer64.c b/src/integer64.c index eba1a4e6..08dc44cc 100644 --- a/src/integer64.c +++ b/src/integer64.c @@ -2,7 +2,7 @@ # C-Code # S3 atomic 64bit integers for R # (c) 2011-2024 Jens Oehlschägel -# (c) 2025 Michael Chirico +# (c) 2025-2026 Michael Chirico # Licence: GPL2 # Provided 'as is', use at your own risk # Created: 2011-12-11 @@ -227,11 +227,13 @@ SEXP as_integer64_bitstring(SEXP x_, SEXP ret_){ for(i=0; iBITS_INTEGER64){ + // # nocov start: only reachable with invalid bitstring input + if (l > BITS_INTEGER64){ ret[i] = NA_INTEGER64; naflag = TRUE; break; } + // # nocov end mask = 1; v = 0; for (k=l-1; k>=0; k--){ @@ -243,13 +245,13 @@ SEXP as_integer64_bitstring(SEXP x_, SEXP ret_){ ret[i] = v; R_CheckUserInterrupt(); } - if (naflag)warning(BITSTRING_OVERFLOW_WARNING); + if (naflag)warning(BITSTRING_OVERFLOW_WARNING); // # nocov return ret_; } -__attribute__((no_sanitize("signed-integer-overflow"))) SEXP plus_integer64(SEXP e1_, SEXP e2_, SEXP ret_){ +SEXP plus_integer64(SEXP e1_, SEXP e2_, SEXP ret_){ long long i, n = LENGTH(ret_); long long i1, n1 = LENGTH(e1_); long long i2, n2 = LENGTH(e2_); @@ -264,7 +266,7 @@ __attribute__((no_sanitize("signed-integer-overflow"))) SEXP plus_integer64(SEXP return ret_; } -__attribute__((no_sanitize("signed-integer-overflow"))) SEXP minus_integer64(SEXP e1_, SEXP e2_, SEXP ret_){ +SEXP minus_integer64(SEXP e1_, SEXP e2_, SEXP ret_){ long long i, n = LENGTH(ret_); long long i1, n1 = LENGTH(e1_); long long i2, n2 = LENGTH(e2_); @@ -279,7 +281,7 @@ __attribute__((no_sanitize("signed-integer-overflow"))) SEXP minus_integer64(SEX return ret_; } -__attribute__((no_sanitize("signed-integer-overflow"))) SEXP diff_integer64(SEXP x_, SEXP lag_, SEXP n_, SEXP ret_){ +SEXP diff_integer64(SEXP x_, SEXP lag_, SEXP n_, SEXP ret_){ long long i, n = *((long long *) REAL(n_)); long long * x = (long long *) REAL(x_); long long * lag = (long long *) REAL(lag_); @@ -326,7 +328,7 @@ SEXP mod_integer64(SEXP e1_, SEXP e2_, SEXP ret_){ } -__attribute__((no_sanitize("signed-integer-overflow"))) SEXP times_integer64_integer64(SEXP e1_, SEXP e2_, SEXP ret_){ +SEXP times_integer64_integer64(SEXP e1_, SEXP e2_, SEXP ret_){ long long i, n = LENGTH(ret_); long long i1, n1 = LENGTH(e1_); long long i2, n2 = LENGTH(e2_); @@ -612,36 +614,31 @@ SEXP sum_integer64(SEXP e1_, SEXP na_rm_, SEXP ret_){ long long i, n = LENGTH(e1_); long long * e1 = (long long *) REAL(e1_); long long * ret = (long long *) REAL(ret_); - long long cumsum, tempsum; - cumsum = 0; - if (asLogical(na_rm_)){ - for(i=0; i 0) ? (((long double) addValue) < ((long double) tempsum)) : ! (((long double) addValue) < ((long double) tempsum)))){ + if(add64_overflow(cumsum, addValue, &cumsum)){ naflag = TRUE; cumsum = NA_INTEGER64; break; } - cumsum = tempsum; } ret[i + j*nrow1] = cumsum; } @@ -1125,7 +1112,7 @@ SEXP matmult_double_integer64(SEXP x_, SEXP y_, SEXP ret_){ long long * y = (long long *) REAL(y_); long long * ret = (long long *) REAL(ret_); Rboolean naflag = FALSE; - long long cumsum, tempsum, addValue; + long long cumsum, addValue; long double longret; for(i=0; i +#include + #define NA_INTEGER64 LLONG_MIN #define ISNA_INTEGER64(X)((X)==NA_INTEGER64) -#define MIN_INTEGER64 LLONG_MIN+1 +#define MIN_INTEGER64 (LLONG_MIN+1) #define MAX_INTEGER64 LLONG_MAX -#define MIN_INTEGER32 INT_MIN+1 +#define MIN_INTEGER32 (INT_MIN+1) #define MAX_INTEGER32 INT_MAX #define LEFTBIT_INTEGER64 ((unsigned long long int)0x8000000000000000) #define RIGHTBIT_INTEGER64 ((unsigned long long int)0x0000000000000001) @@ -38,10 +41,6 @@ #define COERCE_INTEGER64 "%lli" #define BITS_INTEGER64 64 -#define OPPOSITE_SIGNS(x, y) ((x < 0) ^ (y < 0)) -#define GOODISUM64(x, y, z) (((x) > 0) ? ((y) < (z)) : ! ((y) < (z))) -#define GOODIDIFF64(x, y, z) (!(OPPOSITE_SIGNS(x, y) && OPPOSITE_SIGNS(x, z))) -#define GOODIPROD64(x, y, z) ((long double) (x) * (long double) (y) == (z)) #define INTEGER32_OVERFLOW_WARNING "NAs produced by integer overflow" #define INTEGER64_OVERFLOW_WARNING "NAs produced by integer64 overflow" #define INTEGER64_DIVISION_BY_ZERO_WARNING "NAs produced due to division by zero" @@ -50,37 +49,97 @@ #define BITSTRING_OVERFLOW_WARNING "bitstrings longer than 64 bytes converted to NA, multibyte-characters not allowed" #define INTEGER64_NA_COERCION_WARNING "NAs introduced by coercion to integer64 range" +#if (defined(__GNUC__) && __GNUC__ >= 5) || (defined(__has_builtin) && __has_builtin(__builtin_add_overflow)) +#define HAVE_BUILTIN_OVERFLOW 1 +#endif + +static inline bool add64_overflow(long long a, long long b, long long *res) { +#ifdef HAVE_BUILTIN_OVERFLOW + long long r; + if (__builtin_add_overflow(a, b, &r) || r == NA_INTEGER64) { + return true; + } + *res = r; + return false; +#else + if ((b > 0 && a > MAX_INTEGER64 - b) || (b < 0 && a < MIN_INTEGER64 - b)) { + return true; + } + *res = a + b; + return false; +#endif +} + +static inline bool sub64_overflow(long long a, long long b, long long *res) { +#ifdef HAVE_BUILTIN_OVERFLOW + long long r; + if (__builtin_sub_overflow(a, b, &r) || r == NA_INTEGER64) { + return true; + } + *res = r; + return false; +#else + if ((b > 0 && a < MIN_INTEGER64 + b) || (b < 0 && a > MAX_INTEGER64 + b)) { + return true; + } + *res = a - b; + return false; +#endif +} + +static inline bool mul64_overflow(long long a, long long b, long long *res) { +#ifdef HAVE_BUILTIN_OVERFLOW + long long r; + if (__builtin_mul_overflow(a, b, &r) || r == NA_INTEGER64) { + return true; + } + *res = r; + return false; +#else + if (a == 0 || b == 0) { + *res = 0; + return false; + } + if (a > 0) { + if (b > 0) { + if (a > MAX_INTEGER64 / b) return true; + } else { + if (b < MIN_INTEGER64 / a) return true; + } + } else { + if (b > 0) { + if (a < MIN_INTEGER64 / b) return true; + } else { + if (a < MAX_INTEGER64 / b) return true; + } + } + *res = a * b; + return false; +#endif +} + #define PLUS64(e1,e2,ret,naflag) \ if (e1 == NA_INTEGER64 || e2 == NA_INTEGER64) \ ret = NA_INTEGER64; \ - else { \ - ret = e1 + e2; \ - if (!GOODISUM64(e1, e2, ret)) \ - ret = NA_INTEGER64; \ - if (ret == NA_INTEGER64) \ - naflag = TRUE; \ + else if (add64_overflow(e1, e2, &ret)) { \ + ret = NA_INTEGER64; \ + naflag = TRUE; \ } #define MINUS64(e1,e2,ret,naflag) \ if (e1 == NA_INTEGER64 || e2 == NA_INTEGER64) \ ret = NA_INTEGER64; \ - else { \ - ret = e1 - e2; \ - if (!GOODIDIFF64(e1, e2, ret)) \ - ret = NA_INTEGER64; \ - if (ret == NA_INTEGER64) \ - naflag = TRUE; \ + else if (sub64_overflow(e1, e2, &ret)) { \ + ret = NA_INTEGER64; \ + naflag = TRUE; \ } #define PROD64(e1,e2,ret,naflag) \ if (e1 == NA_INTEGER64 || e2 == NA_INTEGER64) \ ret = NA_INTEGER64; \ - else { \ - ret = e1 * e2; \ - if (!GOODIPROD64(e1, e2, ret)) \ - ret = NA_INTEGER64; \ - if (ret == NA_INTEGER64) \ - naflag = TRUE; \ + else if (mul64_overflow(e1, e2, &ret)) { \ + ret = NA_INTEGER64; \ + naflag = TRUE; \ } #define PROD64REAL(e1,e2,ret,naflag,longret) \ diff --git a/tests/testthat/test-integer64.R b/tests/testthat/test-integer64.R index b4317096..6d9d5aaf 100644 --- a/tests/testthat/test-integer64.R +++ b/tests/testthat/test-integer64.R @@ -59,6 +59,17 @@ test_that("integer64 coercion to/from other types works for NA", { expect_identical(as.integer64(NA_integer_), NA_integer64_) expect_identical(as.integer64(NA_real_), NA_integer64_) expect_identical(as.integer64(NA_character_), NA_integer64_) + + # Boundary and warning conditions for conversions + expect_warning( + expect_identical(as.double(as.integer64("18014398509481984")), 18014398509481984.0), + "integer precision lost while converting to double" + ) + expect_warning( + expect_identical(as.integer(as.integer64(2L)^33.0), NA_integer_), + "NAs produced by integer overflow" + ) + expect_identical(mean(as.integer64(1:5), na.rm = FALSE), as.integer64(3L)) }) test_that("integer64 coercion to/from factor types works", { diff --git a/tests/testthat/test-matrix64.R b/tests/testthat/test-matrix64.R index 143a2852..44732c2a 100644 --- a/tests/testthat/test-matrix64.R +++ b/tests/testthat/test-matrix64.R @@ -274,6 +274,14 @@ test_that("matrix multiplication", { expect_identical(matrix(x, 1L)%*%matrix(x, ncol=1L), matrix(NA_integer64_, 1L, 1L)), "NAs produced by integer64 overflow" ) + expect_warning( + expect_identical(matrix(4e9, 1L, 1L)%*%matrix(x, ncol=1L), matrix(NA_integer64_, 1L, 1L)), + "NAs produced by integer64 overflow" + ) + expect_warning( + expect_identical(matrix(x, 1L)%*%matrix(4e9, ncol=1L), matrix(NA_integer64_, 1L, 1L)), + "NAs produced by integer64 overflow" + ) # warning in summation part x = rep_len(as.integer64("3000000000"), 2L) # x**2 < 2^63, but 2 * x**2 > 2^63 @@ -281,6 +289,28 @@ test_that("matrix multiplication", { expect_identical(matrix(x, 1L)%*%matrix(x, ncol=1L), matrix(NA_integer64_, 1L, 1L)), "NAs produced by integer64 overflow" ) + expect_warning( + expect_identical(matrix(c(3e9, 3e9), 1L, 2L)%*%matrix(x, ncol=1L), matrix(NA_integer64_, 1L, 1L)), + "NAs produced by integer64 overflow" + ) + expect_warning( + expect_identical(matrix(x, 1L)%*%matrix(c(3e9, 3e9), ncol=1L), matrix(NA_integer64_, 1L, 1L)), + "NAs produced by integer64 overflow" + ) + + # NA propagation in matrix multiplication + m_na64 = matrix(c(as.integer64(1L), NA_integer64_), 1L, 2L) + m_val64 = matrix(as.integer64(c(1L, 1L)), 2L, 1L) + m_na_dbl = matrix(c(1.0, NA_real_), 1L, 2L) + m_val_dbl = matrix(c(1.0, 1.0), 2L, 1L) + + expect_no_warning(expect_identical(m_na64 %*% m_val64, matrix(NA_integer64_, 1L, 1L))) + expect_no_warning(expect_identical(m_na_dbl %*% m_val64, matrix(NA_integer64_, 1L, 1L))) + expect_no_warning(expect_identical(m_na64 %*% m_val_dbl, matrix(NA_integer64_, 1L, 1L))) + expect_no_warning( + expect_identical(t(m_val_dbl) %*% matrix(c(as.integer64(1L), NA_integer64_), 2L, 1L), matrix(NA_integer64_, 1L, 1L)) + ) + expect_no_warning(expect_identical(t(m_val64) %*% matrix(c(1.0, NA_real_), 2L, 1L), matrix(NA_integer64_, 1L, 1L))) }) diff --git a/tests/testthat/test-ops64.R b/tests/testthat/test-ops64.R index 624483d0..c9ac0b99 100644 --- a/tests/testthat/test-ops64.R +++ b/tests/testthat/test-ops64.R @@ -147,8 +147,6 @@ test_that("Minus and plus", { }) test_that("Minus and plus edge cases and 'rev'", { - # UBSAN signed integer overflow expected for type 'long long int' - # This is a false UBSAN alarm because overflow is detected and NA returned expect_warning( expect_true(identical.integer64( lim.integer64() + 1.0 - 1.0, @@ -167,6 +165,97 @@ test_that("Minus and plus edge cases and 'rev'", { ) }) +test_that("Overflow edge cases for arithmetic and summary functions", { + max_val = lim.integer64()[2L] + min_val = lim.integer64()[1L] + half_min = as.integer64("-4611686018427387904") # -2^62 + half_max = as.integer64("4611686018427387904") # 2^62 + + overflow_warning = "NAs produced by integer64 overflow" + + # True C-level overflow (above LLONG_MAX or below LLONG_MIN) + expect_warning(expect_identical(max_val + 1L, NA_integer64_), overflow_warning) + expect_warning(expect_identical(min_val + (-2L), NA_integer64_), overflow_warning) + expect_warning(expect_identical(max_val - (-1L), NA_integer64_), overflow_warning) + expect_warning(expect_identical(min_val - 2L, NA_integer64_), overflow_warning) + expect_warning(expect_identical(max_val * 2L, NA_integer64_), overflow_warning) + expect_warning(expect_identical(min_val * 2L, NA_integer64_), overflow_warning) + expect_warning(expect_identical(min_val * (-2L), NA_integer64_), overflow_warning) + expect_warning(expect_identical(diff(c(max_val, min_val - 1L)), NA_integer64_), overflow_warning) + + # R-specific overflow (result is exactly LLONG_MIN == NA_INTEGER64, so C arithmetic + # works, but overflow is nevertheless reported as R's integer64 class has narrower range + expect_warning(expect_identical(min_val + (-1L), NA_integer64_), overflow_warning) + expect_warning(expect_identical(half_min + half_min, NA_integer64_), overflow_warning) + expect_warning(expect_identical(min_val - 1L, NA_integer64_), overflow_warning) + expect_warning(expect_identical(as.integer64(-2L) * half_max, NA_integer64_), overflow_warning) + expect_warning( + expect_identical(as.integer64("-2147483648") * as.integer64("4294967296"), NA_integer64_), + overflow_warning + ) + expect_warning(expect_identical(diff(c(as.integer64(1L), min_val)), NA_integer64_), overflow_warning) + expect_warning(expect_identical(sum(min_val, -1L), NA_integer64_), overflow_warning) + expect_warning(expect_identical(sum(c(half_min, half_min)), NA_integer64_), overflow_warning) + expect_warning(expect_identical(prod(as.integer64(-2L), half_max), NA_integer64_), overflow_warning) + expect_warning(expect_identical(cumsum(c(min_val, -1L)), c(min_val, NA_integer64_)), overflow_warning) + expect_warning( + expect_identical(cumprod(c(as.integer64(-2L), half_max)), c(as.integer64(-2L), NA_integer64_)), + overflow_warning + ) + + # Boundary cases that _don't_ overflow + expect_identical(min_val + 0L, min_val) + expect_identical(min_val + 1L, as.integer64("-9223372036854775806")) + expect_identical(half_min + (half_min + 1L), min_val) + expect_identical(min_val - 0L, min_val) + expect_identical(max_val - 0L, max_val) + expect_identical(min_val * 1L, min_val) + expect_identical(min_val * (-1L), max_val) + expect_identical(max_val * (-1L), min_val) + expect_identical(sum(min_val, 0L), min_val) + expect_identical(prod(min_val, 1L), min_val) + + # If NA is present, NA is returned without overflow + expect_no_warning(expect_identical(NA_integer64_ + 1L, NA_integer64_)) + expect_no_warning(expect_identical(1L + NA_integer64_, NA_integer64_)) + expect_no_warning(expect_identical(NA_integer64_ - 1L, NA_integer64_)) + expect_no_warning(expect_identical(1L - NA_integer64_, NA_integer64_)) + expect_no_warning(expect_identical(NA_integer64_ * 2L, NA_integer64_)) + expect_no_warning(expect_identical(2L * NA_integer64_, NA_integer64_)) + expect_no_warning(expect_identical(sum(NA_integer64_), NA_integer64_)) + expect_no_warning(expect_identical(sum(c(as.integer64(1L), NA_integer64_), na.rm = FALSE), NA_integer64_)) + expect_no_warning(expect_identical(sum(c(as.integer64(1:2), NA_integer64_), na.rm = TRUE), as.integer64(3L))) + expect_warning( + expect_identical(sum(c(max_val, as.integer64(1L), NA_integer64_), na.rm = TRUE), NA_integer64_), + overflow_warning + ) + expect_no_warning(expect_identical(prod(NA_integer64_), NA_integer64_)) + expect_no_warning(expect_identical(prod(c(as.integer64(1L), NA_integer64_), na.rm = FALSE), NA_integer64_)) + expect_no_warning(expect_identical(prod(c(as.integer64(2:3), NA_integer64_), na.rm = TRUE), as.integer64(6L))) + expect_warning( + expect_identical(prod(c(max_val, as.integer64(2L), NA_integer64_), na.rm = TRUE), NA_integer64_), + overflow_warning + ) + expect_no_warning(expect_identical(cumsum(c(NA_integer64_, 1L)), c(NA_integer64_, NA_integer64_))) + expect_no_warning(expect_identical(cumprod(c(NA_integer64_, 1L)), c(NA_integer64_, NA_integer64_))) + expect_no_warning(expect_identical(diff(c(NA_integer64_, as.integer64(1L))), NA_integer64_)) + expect_no_warning(expect_identical(diff(c(as.integer64(1L), NA_integer64_)), NA_integer64_)) + + # Vectorized operations with partial overflow + expect_warning( + expect_identical(c(max_val, as.integer64(1L)) + 1L, c(NA_integer64_, as.integer64(2L))), + overflow_warning + ) + expect_warning( + expect_identical(c(min_val, as.integer64(1L)) - 1L, c(NA_integer64_, as.integer64(0L))), + overflow_warning + ) + expect_warning( + expect_identical(c(max_val, as.integer64(1L)) * 2L, c(NA_integer64_, as.integer64(2L))), + overflow_warning + ) +}) + test_that("Logical operators", { expect_true(identical.integer64( !c(NA, -1:1),