From 0ff827eb8e79610827df6d43438f99a35bcd7609 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Tue, 2 Jun 2026 08:08:29 +0200 Subject: [PATCH] =?UTF-8?q?fix(core):=20promote=20to=20BigInt=20instead=20?= =?UTF-8?q?of=20panicking=20on=20neg/abs=20of=20i64::MIN=20=F0=9F=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- ndc_core/src/int.rs | 10 ++++++++-- .../programs/001_math/032_neg_abs_i64_min.ndc | 8 ++++++++ .../900_bugs/bug0026_neg_abs_i64_min_overflow.ndc | 6 ++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 tests/functional/programs/001_math/032_neg_abs_i64_min.ndc create mode 100644 tests/functional/programs/900_bugs/bug0026_neg_abs_i64_min_overflow.ndc diff --git a/ndc_core/src/int.rs b/ndc_core/src/int.rs index fe2218a8..1b583013 100644 --- a/ndc_core/src/int.rs +++ b/ndc_core/src/int.rs @@ -149,7 +149,10 @@ impl Int { #[must_use] pub fn abs(&self) -> Self { match self { - Self::Int64(i) => Self::from(i.abs()), + // `i64::MIN.abs()` overflows, so promote to BigInt in that case. + Self::Int64(i) => i + .checked_abs() + .map_or_else(|| Self::from(BigInt::from(*i).abs()), Self::Int64), Self::BigInt(b) => Self::from(b.abs()), } } @@ -218,7 +221,10 @@ impl Neg for Int { fn neg(self) -> Self::Output { match self { - Self::Int64(i) => Self::Int64(i.neg()), + // `-i64::MIN` overflows, so promote to BigInt in that case. + Self::Int64(i) => i + .checked_neg() + .map_or_else(|| Self::BigInt(BigInt::from(i).neg()), Self::Int64), Self::BigInt(i) => Self::BigInt(i.neg()), } } diff --git a/tests/functional/programs/001_math/032_neg_abs_i64_min.ndc b/tests/functional/programs/001_math/032_neg_abs_i64_min.ndc new file mode 100644 index 00000000..b24e23a9 --- /dev/null +++ b/tests/functional/programs/001_math/032_neg_abs_i64_min.ndc @@ -0,0 +1,8 @@ +// Negating or taking the absolute value of `i64::MIN` overflows the i64 +// range (`-i64::MIN` does not fit in an i64). `Int::neg` and `Int::abs` used +// to call `i.neg()` / `i.abs()` directly, which panics in debug builds and +// silently wraps in release. They now promote to BigInt, yielding the correct +// positive magnitude. +// expect-output: 9223372036854775808 9223372036854775808 9223372036854775808 +let min = -9223372036854775807 - 1; +print(-min, abs(min), abs(-min)); diff --git a/tests/functional/programs/900_bugs/bug0026_neg_abs_i64_min_overflow.ndc b/tests/functional/programs/900_bugs/bug0026_neg_abs_i64_min_overflow.ndc new file mode 100644 index 00000000..595baa86 --- /dev/null +++ b/tests/functional/programs/900_bugs/bug0026_neg_abs_i64_min_overflow.ndc @@ -0,0 +1,6 @@ +// `abs(i64::MIN)` and `-(i64::MIN)` overflow the i64 range because the +// positive magnitude `2^63` does not fit in an i64. `Int::abs`/`Int::neg` +// used to evaluate `i.abs()`/`i.neg()` directly, panicking with "attempt to +// negate with overflow" in debug builds. They now promote to BigInt. +// expect-output: 9223372036854775808 +print(abs(-9223372036854775807 - 1));