Skip to content
Closed
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
7 changes: 7 additions & 0 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,7 @@ of them as a value.
| Builtin | Arguments | Result |
| ------- | --------- | ------ |
| `abs(n)` | 1 | The magnitude. |
| `mod(n, d)` | 2 | The floored remainder of `n` by `d`. The sign follows `d`. A zero divisor is an error, with the same words `%` uses. |
| `min(...)` | 1 or more | The smallest. |
| `max(...)` | 1 or more | The largest. |
| `floor(n)` | 1 | The largest integer that is not larger than `n`. Gives an `int`. |
Expand All @@ -1353,6 +1354,12 @@ of them as a value.
| `sum(a)` | 1 array | The numbers in the array, added. An empty array gives `0`. |
| `product(a)` | 1 array | The numbers in the array, multiplied. An empty array gives `1`. |


`mod` is the floored remainder. `%` truncates toward zero, so `-7 % 3` is `-1`.
`mod(-7, 3)` is `2`. The sign of `mod` follows the divisor: `mod(1, -10)` is
`-9`. Use `mod` for wrapping (ring buffers, clocks, hash buckets); keep `%`
when the truncated form is what you want.

`sum` and `product` give an `int` for an array of integers, and a `float` for
an array that has one or more floats. Section 5.2 gives the promotion rule.
Overflow of the integer form is an error, as it is for each other integer
Expand Down
116 changes: 107 additions & 9 deletions src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub fn register(globals: &mut Globals) {
define(globals, "slice", slice);
define(globals, "reverse", reverse);
define(globals, "abs", abs);
define(globals, "mod", modulo);
define(globals, "min", min);
define(globals, "max", max);
define(globals, "floor", floor);
Expand Down Expand Up @@ -1138,6 +1139,73 @@ fn abs(_out: &mut dyn Output, _input: &mut dyn Input, args: Vec<Value>) -> Resul
}
}


/// Floored remainder of two integers: sign follows the divisor, never the
/// dividend. Differs from `%`, which truncates toward zero.
///
/// `a.checked_rem(b)` is the truncated form. When the truncated remainder is
/// non-zero and the operands have different signs, adding `b` lands on the
/// floored answer. That is the adjustment every wrapping use wants:
/// `mod(-1, 10)` is `9`, not `-1`.
fn floored_rem_i64(a: i64, b: i64) -> Option<i64> {
let r = a.checked_rem(b)?;
if r != 0 && (a < 0) != (b < 0) {
r.checked_add(b)
} else {
Some(r)
}
}

/// Floored remainder of two floats. Same definition as the integer form:
/// `a - b * floor(a / b)`. A zero divisor is refused by the caller.
fn floored_rem_f64(a: f64, b: f64) -> f64 {
a - b * (a / b).floor()
}

/// `mod(n, d)` is the floored remainder of `n` by `d`. The sign follows `d`,
/// so a negative dividend never produces a negative result when `d` is positive.
///
/// `%` keeps the truncating form required by section 5 (and by every language
/// that maps remainder onto machine division). Wrapping arithmetic needs the
/// other definition, and writing `((n % d) + d) % d` by hand is three operations
/// and two mentions of `d` for one idea. `mod` is that idea under one name.
///
/// A zero divisor refuses with the same words `%` uses. Two integers stay
/// integers; any float promotes, matching every other binary numeric builtin.
fn modulo(
_out: &mut dyn Output,
_input: &mut dyn Input,
args: Vec<Value>,
) -> Result<Value, String> {
check_arity("mod", &args, 2)?;
match (&args[0], &args[1]) {
(Value::Int(n), Value::Int(d)) => {
if *d == 0 {
return Err("modulo by zero".to_string());
}
floored_rem_i64(*n, *d)
.map(Value::Int)
.ok_or_else(|| "integer overflow in mod".to_string())
}
(Value::Int(_) | Value::Float(_), Value::Int(_) | Value::Float(_)) => {
let n = number_arg("mod", &args[0])?;
let d = number_arg("mod", &args[1])?;
if d == 0.0 {
return Err("modulo by zero".to_string());
}
Ok(Value::Float(floored_rem_f64(n, d)))
}
(other, _) if !matches!(other, Value::Int(_) | Value::Float(_)) => Err(format!(
"mod expects numbers but got a {}",
other.type_name()
)),
(_, other) => Err(format!(
"mod expects numbers but got a {}",
other.type_name()
)),
}
}

/// Shared implementation of `min` and `max`: pick the argument that compares as
/// `want` against the running best. Preserves the winning value's own type.
///
Expand Down Expand Up @@ -1768,14 +1836,14 @@ impl Args {
///
/// This is the last per-element heap allocation left in the engine, and it
/// only happens when a *builtin* is the callback, as in `map(xs, abs)`.
/// Removing it means changing [`BuiltinFn`] for the forty builtins
/// Removing it means changing [`BuiltinFn`] for the forty-eight builtins
/// registered with `define`, several of which move values out of the vector
/// they are handed, which is a larger change than it looks and is left for
/// later.
///
/// Forty-one is the count of `define` calls, not the count of builtins. The
/// two were the same figure until 1.1 and are not any more: there are
/// sixty-one builtins, of which four take a `SystemFn`, twelve take an
/// Forty-eight is the count of `define` calls, not the count of builtins.
/// The two were the same figure until 1.1 and are not any more: there are
/// sixty-eight builtins, of which four take a `SystemFn`, twelve take an
/// `AmbientFn`, and four take a `HostFn`, and none of those twenty would
/// be touched by such a change.
pub fn into_vec(self) -> Vec<Value> {
Expand Down Expand Up @@ -2619,6 +2687,35 @@ mod tests {
assert_eq!(out("print(abs(-3), abs(-2.5), abs(4))"), "3 2.5 4\n");
}

#[test]
fn mod_follows_the_divisor_and_wraps_a_negative_dividend() {
// Floored remainder: positive divisor, negative dividend lands in
// `[0, d)` rather than the negative truncated remainder `%` gives.
assert_eq!(out("print(mod(-1, 10), mod(-7, 3), mod(7, 3))"), "9 2 1\n");
// Negative divisor: sign follows the divisor under the floored rule.
assert_eq!(out("print(mod(1, -10), mod(7, -3), mod(-7, -3))"), "-9 -2 -1\n");
// Floats match, including mixed int/float promotion.
assert_eq!(
out("print(mod(-7.0, 3.0), mod(-1, 10.0), mod(7.5, 2.0))"),
"2.0 9.0 1.5\n"
);
// Same answer as `%` when both operands are non-negative.
assert_eq!(out("print(mod(7, 3), 7 % 3)"), "1 1\n");
}

#[test]
fn mod_refuses_a_zero_divisor() {
assert_eq!(err("mod(1, 0)"), "modulo by zero");
assert_eq!(err("mod(1.0, 0.0)"), "modulo by zero");
}

#[test]
fn mod_refuses_non_numbers() {
assert!(err("mod(\"a\", 2)").contains("expects numbers"));
assert!(err("mod(2, \"a\")").contains("expects numbers"));
assert!(err("mod(1)").contains("2 arguments"));
}

#[test]
fn min_and_max_over_numbers() {
assert_eq!(out("print(min(3, 1, 2), max(3, 1, 2))"), "1 3\n");
Expand Down Expand Up @@ -2927,8 +3024,8 @@ mod count {
}

assert_eq!(
plain, 47,
"{plain} builtins take a BuiltinFn, not 47. Quoted by `Args::into_vec` \
plain, 48,
"{plain} builtins take a BuiltinFn, not 48. Quoted by `Args::into_vec` \
above and by the trampoline section of docs/architecture.md."
);
assert_eq!(
Expand All @@ -2938,8 +3035,8 @@ mod count {
);
assert_eq!(
plain + system + ambient,
63,
"{} builtins reach `call_native`, not 63. Quoted by the caught-error \
64,
"{} builtins reach `call_native`, not 64. Quoted by the caught-error \
guard in `Vm::call_native`.",
plain + system + ambient
);
Expand All @@ -2959,7 +3056,7 @@ mod count {
/// Every builtin, in registration order. Pinned so the count cannot drift and
/// so the specification has one list to be generated from rather than a second
/// hand-written one that can disagree.
pub const BUILTIN_NAMES: [&str; 67] = [
pub const BUILTIN_NAMES: [&str; 68] = [
"print",
"eprint",
"exit",
Expand Down Expand Up @@ -2995,6 +3092,7 @@ pub const BUILTIN_NAMES: [&str; 67] = [
"slice",
"reverse",
"abs",
"mod",
"min",
"max",
"floor",
Expand Down