diff --git a/manual/src/reference/types/number.md b/manual/src/reference/types/number.md index 4b86211d..604698d0 100644 --- a/manual/src/reference/types/number.md +++ b/manual/src/reference/types/number.md @@ -30,6 +30,15 @@ Andy C++ has four number types: | `>=<` | Reverse compare | `false` | `false` | | `<>` | Concatenate string values | `true` | `false` | +### Division by zero + +Dividing by zero with `/` or `\` (floor division) follows floating-point +semantics: the result is promoted to a float and yields infinity (for example +`1 / 0` and `1 \ 0` are both `inf`). The remainder operators `%` (modulo) and +`%%` (euclidean remainder) have no meaningful result for a zero divisor, so they +raise a runtime error (`division by zero`). Floating-point `%` follows IEEE +semantics and returns `NaN`. + Integers also support these operations: | Operator | Function | Support augmented assignment [[1]](../../features/augmented-assignment.md) | Augmentable with `not` | diff --git a/ndc_core/src/num.rs b/ndc_core/src/num.rs index 4633d7f1..1584477b 100644 --- a/ndc_core/src/num.rs +++ b/ndc_core/src/num.rs @@ -314,7 +314,71 @@ macro_rules! impl_binary_operator_all { impl_binary_operator_all!(Add, add, Add::add, Add::add, Add::add, Add::add); impl_binary_operator_all!(Sub, sub, Sub::sub, Sub::sub, Sub::sub, Sub::sub); impl_binary_operator_all!(Mul, mul, Mul::mul, Mul::mul, Mul::mul, Mul::mul); -impl_binary_operator_all!(Rem, rem, Rem::rem, Rem::rem, Rem::rem, Rem::rem); + +/// Returns `true` for the number kinds that use exact (integer/rational) +/// arithmetic. Remainder of an exact value by zero panics in `num` and +/// `num-bigint`; floats and complex numbers produce `NaN` instead, so they +/// are handled by the normal arithmetic path. +fn is_exact(n: &Number) -> bool { + matches!(n, Number::Int(_) | Number::Rational(_)) +} + +impl Rem for Number { + type Output = Result; + + fn rem(self, rhs: Self) -> Self::Output { + // Reject exact remainder by zero up front; without this the integer + // and rational arms below panic ("attempt to divide by zero"). + if is_exact(&self) && is_exact(&rhs) && rhs.is_zero() { + return Err(BinaryOperatorError::new("division by zero".to_string())); + } + Ok(match (self, rhs) { + // Integer + (Self::Int(left), Self::Int(right)) => Self::Int(left % right), + // Complex + (Self::Complex(left), right) => Self::Complex(left % right.to_complex()), + (left, Self::Complex(right)) => Self::Complex(left.to_complex() % right), + // Float + (Self::Float(left), right) => { + Self::Float(left % right.to_f64().expect("cannot convert complex to float")) + } + (left, Self::Float(right)) => { + Self::Float(left.to_f64().expect("cannot convert complex to float") % right) + } + // Rational + (left, Self::Rational(right)) => Self::rational( + left.to_rational().expect("cannot convert to rational") % right.unbox(), + ), + (Self::Rational(left), right) => Self::rational( + left.unbox() % right.to_rational().expect("cannot convert to rational"), + ), + }) + } +} + +impl Rem<&Self> for Number { + type Output = Result; + + fn rem(self, rhs: &Self) -> Self::Output { + self % rhs.clone() + } +} + +impl Rem for &Number { + type Output = Result; + + fn rem(self, rhs: Number) -> Self::Output { + self.clone() % rhs + } +} + +impl Rem<&Number> for &Number { + type Output = Result; + + fn rem(self, rhs: &Number) -> Self::Output { + self.clone() % rhs.clone() + } +} impl Div<&Number> for &Number { type Output = Number; @@ -377,12 +441,26 @@ impl Number { } } + #[must_use] + pub fn is_zero(&self) -> bool { + match self { + Self::Int(i) => i.is_zero(), + Self::Float(f) => *f == 0.0, + Self::Rational(r) => r.is_zero(), + Self::Complex(c) => c.is_zero(), + } + } + pub fn checked_rem_euclid(self, rhs: Self) -> Result { match (self, rhs) { - (Self::Int(p1), Self::Int(p2)) => p1 - .checked_rem_euclid(&p2) - .ok_or(BinaryOperatorError::new("operation failed".to_string())) - .map(Self::Int), + (Self::Int(p1), Self::Int(p2)) => { + if p2.is_zero() { + return Err(BinaryOperatorError::new("division by zero".to_string())); + } + p1.checked_rem_euclid(&p2) + .ok_or(BinaryOperatorError::new("operation failed".to_string())) + .map(Self::Int) + } (Self::Float(p1), Self::Float(p2)) => Ok(Self::Float(p1.rem_euclid(p2))), (left, right) => Err(BinaryOperatorError::undefined_operation( @@ -395,10 +473,16 @@ impl Number { pub fn floor_div(self, rhs: Self) -> Result { match (self, rhs) { - // Handle this case separately because it's faster?? - (Self::Int(Int::Int64(l)), Self::Int(Int::Int64(r))) => { - Ok(Self::Int(Int::Int64(l.div_euclid(r)))) - } + // Fast path for two i64s. `div_euclid` panics on a zero divisor + // (and on `i64::MIN / -1`), so fall back to the general path in + // those cases — it promotes to a float and yields infinity for a + // zero divisor, matching `/` and the BigInt/rational paths. + (Self::Int(Int::Int64(l)), Self::Int(Int::Int64(r))) => match l.checked_div_euclid(r) { + Some(q) => Ok(Self::Int(Int::Int64(q))), + None => Self::Int(Int::Int64(l)) + .div(Self::Int(Int::Int64(r))) + .map(|n| n.floor()), + }, (l, r) => Ok(l.div(r)?.floor()), } } diff --git a/ndc_stdlib/src/math.rs b/ndc_stdlib/src/math.rs index 5732c824..e19da491 100644 --- a/ndc_stdlib/src/math.rs +++ b/ndc_stdlib/src/math.rs @@ -301,12 +301,46 @@ pub mod f64 { std::ops::Mul::mul, "Multiplies two integers." ); - implement_binary_operator_on_int!( - "%", - checked_rem, - std::ops::Rem::rem, - "Returns the remainder of dividing two integers." - ); + // Integer remainder needs an explicit division-by-zero guard: the + // generic fast-path fallback (`Int % Int`) panics on a zero divisor. + env.declare_global_fn(Rc::new(NativeFunction { + name: "%".to_string(), + documentation: Some("Returns the remainder of dividing two integers.".to_string()), + static_type: StaticType::Function { + parameters: Some(vec![StaticType::Int, StaticType::Int]), + return_type: Box::new(StaticType::Int), + }, + func: NativeFunc::Simple(Box::new(|args| match args { + [Value::Int(l), Value::Int(r)] => { + if *r == 0 { + return Err(VmError::native("division by zero".to_string())); + } + if let Some(result) = l.checked_rem(*r) { + Ok(Value::Int(result)) + } else { + // The fast path only overflows for `i64::MIN % -1`, + // whose mathematical result is 0; fall back to Int. + Ok(Value::from_int(Int::Int64(*l) % Int::Int64(*r))) + } + } + [left, right] => { + let l = left.to_int().ok_or_else(|| { + VmError::native(format!("expected int, got {}", left.static_type())) + })?; + let r = right.to_int().ok_or_else(|| { + VmError::native(format!("expected int, got {}", right.static_type())) + })?; + if r.is_zero() { + return Err(VmError::native("division by zero".to_string())); + } + Ok(Value::from_int(l % r)) + } + _ => Err(VmError::native(format!( + "expected 2 arguments, got {}", + args.len() + ))), + })), + })); // Float-specific overloads: operate directly on f64. macro_rules! implement_binary_operator_on_float { diff --git a/tests/functional/programs/001_math/024_modulo_by_zero.ndc b/tests/functional/programs/001_math/024_modulo_by_zero.ndc new file mode 100644 index 00000000..560bbd17 --- /dev/null +++ b/tests/functional/programs/001_math/024_modulo_by_zero.ndc @@ -0,0 +1,2 @@ +// expect-error: division by zero +print(5 % 0); diff --git a/tests/functional/programs/001_math/025_modulo_by_zero_bigint.ndc b/tests/functional/programs/001_math/025_modulo_by_zero_bigint.ndc new file mode 100644 index 00000000..2c81be23 --- /dev/null +++ b/tests/functional/programs/001_math/025_modulo_by_zero_bigint.ndc @@ -0,0 +1,2 @@ +// expect-error: division by zero +print(170141183460469231731687303715884105727 % 0); diff --git a/tests/functional/programs/001_math/026_floor_div_by_zero.ndc b/tests/functional/programs/001_math/026_floor_div_by_zero.ndc new file mode 100644 index 00000000..0c926052 --- /dev/null +++ b/tests/functional/programs/001_math/026_floor_div_by_zero.ndc @@ -0,0 +1,4 @@ +// Floor division by zero promotes to a float and yields infinity, the same +// as `/` (it must not panic). +print(5 \ 0); +// expect-output: inf diff --git a/tests/functional/programs/001_math/027_euclidean_modulo_by_zero.ndc b/tests/functional/programs/001_math/027_euclidean_modulo_by_zero.ndc new file mode 100644 index 00000000..5fd9ae6a --- /dev/null +++ b/tests/functional/programs/001_math/027_euclidean_modulo_by_zero.ndc @@ -0,0 +1,2 @@ +// expect-error: division by zero +print(5 %% 0); diff --git a/tests/functional/programs/001_math/028_rational_modulo_by_zero.ndc b/tests/functional/programs/001_math/028_rational_modulo_by_zero.ndc new file mode 100644 index 00000000..7365e79c --- /dev/null +++ b/tests/functional/programs/001_math/028_rational_modulo_by_zero.ndc @@ -0,0 +1,3 @@ +// expect-error: division by zero +let a = 1 / 3; +print(a % 0); diff --git a/tests/functional/programs/001_math/029_floor_div_by_zero_rational.ndc b/tests/functional/programs/001_math/029_floor_div_by_zero_rational.ndc new file mode 100644 index 00000000..74797972 --- /dev/null +++ b/tests/functional/programs/001_math/029_floor_div_by_zero_rational.ndc @@ -0,0 +1,6 @@ +// Regression: rational and BigInt floor division by zero yields infinity, +// not an error (matches `/`). +print((1 / 3) \ 0); +print(170141183460469231731687303715884105727 \ 0); +// expect-output: inf +// expect-output: inf