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