From c3c9521aba7b462c8f6a8a356c738ad94c4ffb2c Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Tue, 2 Jun 2026 08:01:42 +0200 Subject: [PATCH] =?UTF-8?q?fix(core):=20return=20error=20instead=20of=20pa?= =?UTF-8?q?nicking=20on=20negative=20power=20edge=20cases=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/num.rs | 33 ++++++++++++------- .../030_pow_zero_negative_exponent.ndc | 5 +++ ...31_pow_zero_negative_rational_exponent.ndc | 5 +++ ...bug0025_pow_negative_rational_exponent.ndc | 7 ++++ 4 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 tests/functional/programs/001_math/030_pow_zero_negative_exponent.ndc create mode 100644 tests/functional/programs/001_math/031_pow_zero_negative_rational_exponent.ndc create mode 100644 tests/functional/programs/900_bugs/bug0025_pow_negative_rational_exponent.ndc diff --git a/ndc_core/src/num.rs b/ndc_core/src/num.rs index b1f5f7a9..ab3bba33 100644 --- a/ndc_core/src/num.rs +++ b/ndc_core/src/num.rs @@ -487,6 +487,26 @@ impl Number { } } + /// Raise an integer base to a (possibly negative) integer exponent. + /// A negative exponent yields the reciprocal as a rational, except + /// `0 ^ negative`, which is division by zero and returns an error + /// instead of panicking with a zero denominator. + fn int_pow(base: &Int, exponent: &Int) -> Result { + if exponent.is_negative() { + if base.is_zero() { + return Err(BinaryOperatorError::new("division by zero".to_string())); + } + let exponent = exponent.to_bigint(); + let denominator = num::pow::Pow::pow(base.to_bigint(), exponent.magnitude()); + Ok(Self::Rational(Box::new(BigRational::new( + BigInt::from(1), + denominator, + )))) + } else { + Ok(Self::Int(base.pow(exponent))) + } + } + pub fn pow(self, rhs: Self) -> Result { // Reject astronomically large integer exponents up front: an exponent // that doesn't fit in u32 would produce a result too large to compute @@ -505,23 +525,14 @@ impl Number { Ok(match (self, rhs) { // Int vs others - (Self::Int(p1), Self::Int(p2)) => { - if p2.is_negative() { - let p2 = p2.to_bigint(); - let p2 = p2.magnitude(); - let ans = num::pow::Pow::pow(p1.to_bigint(), p2); - Self::Rational(Box::new(BigRational::new(BigInt::from(1), ans))) - } else { - Self::Int(p1.pow(&p2)) - } - } + (Self::Int(p1), Self::Int(p2)) => return Self::int_pow(&p1, &p2), (Self::Int(p1), Self::Float(p2)) => Self::Float(f64::from(p1).powf(p2)), (Self::Int(p1), Self::Complex(p2)) => { Self::Complex(Complex::from(f64::from(p1)).powc(p2)) } (Self::Int(p1), Self::Rational(p2)) => { if p2.is_integer() { - return Ok(Self::Int(p1.pow(&Int::BigInt(p2.to_integer())))); + return Self::int_pow(&p1, &Int::BigInt(p2.to_integer())); } Self::Float(f64::from(p1).powf(rational_to_float(&p2))) diff --git a/tests/functional/programs/001_math/030_pow_zero_negative_exponent.ndc b/tests/functional/programs/001_math/030_pow_zero_negative_exponent.ndc new file mode 100644 index 00000000..5bd2d126 --- /dev/null +++ b/tests/functional/programs/001_math/030_pow_zero_negative_exponent.ndc @@ -0,0 +1,5 @@ +// `0 ^ negative` is division by zero. Raising zero to a negative power +// used to build a rational with a zero denominator, which panicked in +// num-rational ("denominator == 0"). It now reports a recoverable error. +// expect-error: division by zero +print(0 ^ -1); diff --git a/tests/functional/programs/001_math/031_pow_zero_negative_rational_exponent.ndc b/tests/functional/programs/001_math/031_pow_zero_negative_rational_exponent.ndc new file mode 100644 index 00000000..ff912d6f --- /dev/null +++ b/tests/functional/programs/001_math/031_pow_zero_negative_rational_exponent.ndc @@ -0,0 +1,5 @@ +// Same division-by-zero as `0 ^ -1`, but reached through the rational +// exponent path (`2/-1` is an integer-valued rational). Used to panic +// with "right hand side must not be negative"; now a recoverable error. +// expect-error: division by zero +print(0 ^ (2/-1)); diff --git a/tests/functional/programs/900_bugs/bug0025_pow_negative_rational_exponent.ndc b/tests/functional/programs/900_bugs/bug0025_pow_negative_rational_exponent.ndc new file mode 100644 index 00000000..1fbaa17a --- /dev/null +++ b/tests/functional/programs/900_bugs/bug0025_pow_negative_rational_exponent.ndc @@ -0,0 +1,7 @@ +// A negative *integer* exponent that arrives as a rational (e.g. the +// result of `1 / -1`) used to call `Int::pow` with a negative right-hand +// side, which panicked unconditionally ("right hand side must not be +// negative") and aborted the whole process. It now computes the +// reciprocal, exactly like a plain negative integer exponent. +// expect-output: 1/2 +print(2 ^ (1/-1));