Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions manual/src/reference/types/number.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <sup>[[1]](../../features/augmented-assignment.md)</sup> | Augmentable with `not` |
Expand Down
102 changes: 93 additions & 9 deletions ndc_core/src/num.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> for Number {
type Output = Result<Self, BinaryOperatorError>;

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<Self, BinaryOperatorError>;

fn rem(self, rhs: &Self) -> Self::Output {
self % rhs.clone()
}
}

impl Rem<Number> for &Number {
type Output = Result<Number, BinaryOperatorError>;

fn rem(self, rhs: Number) -> Self::Output {
self.clone() % rhs
}
}

impl Rem<&Number> for &Number {
type Output = Result<Number, BinaryOperatorError>;

fn rem(self, rhs: &Number) -> Self::Output {
self.clone() % rhs.clone()
}
}

impl Div<&Number> for &Number {
type Output = Number;
Expand Down Expand Up @@ -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<Self, BinaryOperatorError> {
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(
Expand All @@ -395,10 +473,16 @@ impl Number {

pub fn floor_div(self, rhs: Self) -> Result<Self, BinaryOperatorError> {
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()),
}
}
Expand Down
46 changes: 40 additions & 6 deletions ndc_stdlib/src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions tests/functional/programs/001_math/024_modulo_by_zero.ndc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// expect-error: division by zero
print(5 % 0);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// expect-error: division by zero
print(170141183460469231731687303715884105727 % 0);
4 changes: 4 additions & 0 deletions tests/functional/programs/001_math/026_floor_div_by_zero.ndc
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// expect-error: division by zero
print(5 %% 0);
Comment thread
timfennis marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// expect-error: division by zero
let a = 1 / 3;
print(a % 0);
Original file line number Diff line number Diff line change
@@ -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
Loading