#![deny(clippy::arithmetic_side_effects)]
#![allow(clippy::cast_precision_loss, clippy::module_name_repetitions)]
use crate::compiler::{ExpressionError, value::VrlValueConvert};
use crate::value::{ObjectMap, Value};
use bytes::Bytes;
use bytestring::ByteString;
use ordered_float::NotNan;
use super::ValueError;
#[allow(clippy::missing_errors_doc)]
pub trait VrlValueArithmetic: Sized {
fn try_mul(self, rhs: Self) -> Result<Self, ValueError>;
fn try_div(self, rhs: Self) -> Result<Self, ValueError>;
fn try_add(self, rhs: Self) -> Result<Self, ValueError>;
fn try_sub(self, rhs: Self) -> Result<Self, ValueError>;
fn try_or(self, rhs: impl FnMut() -> Result<Self, ExpressionError>)
-> Result<Self, ValueError>;
fn try_and(self, rhs: Self) -> Result<Self, ValueError>;
fn try_rem(self, rhs: Self) -> Result<Self, ValueError>;
fn try_gt(self, rhs: Self) -> Result<Self, ValueError>;
fn try_ge(self, rhs: Self) -> Result<Self, ValueError>;
fn try_lt(self, rhs: Self) -> Result<Self, ValueError>;
fn try_le(self, rhs: Self) -> Result<Self, ValueError>;
fn try_merge(self, rhs: Self) -> Result<Self, ValueError>;
fn eq_lossy(&self, rhs: &Self) -> bool;
}
fn repeat_string(s: &ByteString, n: usize) -> Value {
let bytes = Bytes::from(s.as_bytes().repeat(n));
Value::String(unsafe { ByteString::from_bytes_unchecked(bytes) })
}
fn float_result(value: f64) -> Result<Value, ValueError> {
NotNan::new(value)
.map(Value::Float)
.map_err(|_| ValueError::NanFloat)
}
impl VrlValueArithmetic for Value {
fn try_mul(self, rhs: Self) -> Result<Self, ValueError> {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let as_usize = |num| if num < 0 { 0 } else { num as usize };
let value = match (self, rhs) {
(Value::Integer(lhs), Value::Bytes(rhs)) => {
Bytes::from(rhs.repeat(as_usize(lhs))).into()
}
(Value::Integer(lhs), Value::String(rhs)) => repeat_string(&rhs, as_usize(lhs)),
(Value::Integer(lhs), Value::Float(rhs)) => {
float_result(lhs as f64 * rhs.into_inner())?
}
(Value::Integer(lhs), Value::Integer(rhs)) => i64::wrapping_mul(lhs, rhs).into(),
(Value::Float(lhs), Value::Integer(rhs)) => {
float_result(lhs.into_inner() * rhs as f64)?
}
(Value::Float(lhs), Value::Float(rhs)) => {
float_result(lhs.into_inner() * rhs.into_inner())?
}
(Value::Bytes(lhs), Value::Integer(rhs)) => {
Bytes::from(lhs.repeat(as_usize(rhs))).into()
}
(Value::String(lhs), Value::Integer(rhs)) => repeat_string(&lhs, as_usize(rhs)),
(lhs, rhs) => return Err(ValueError::Mul(lhs.kind(), rhs.kind())),
};
Ok(value)
}
fn try_div(self, rhs: Self) -> Result<Self, ValueError> {
match (self, rhs) {
(_, Value::Integer(0)) => Err(ValueError::DivideByZero),
(_, Value::Float(rhs)) if rhs.into_inner() == 0.0 => Err(ValueError::DivideByZero),
(Value::Integer(lhs), Value::Integer(rhs)) => float_result(lhs as f64 / rhs as f64),
(Value::Integer(lhs), Value::Float(rhs)) => float_result(lhs as f64 / rhs.into_inner()),
(Value::Float(lhs), Value::Integer(rhs)) => float_result(lhs.into_inner() / rhs as f64),
(Value::Float(lhs), Value::Float(rhs)) => {
float_result(lhs.into_inner() / rhs.into_inner())
}
(lhs, rhs) => Err(ValueError::Div(lhs.kind(), rhs.kind())),
}
}
fn try_add(self, rhs: Self) -> Result<Self, ValueError> {
let value = match (self, rhs) {
(Value::Integer(lhs), Value::Integer(rhs)) => i64::wrapping_add(lhs, rhs).into(),
(Value::Integer(lhs), Value::Float(rhs)) => {
float_result(lhs as f64 + rhs.into_inner())?
}
(Value::Float(lhs), Value::Integer(rhs)) => {
float_result(lhs.into_inner() + rhs as f64)?
}
(Value::Float(lhs), Value::Float(rhs)) => {
float_result(lhs.into_inner() + rhs.into_inner())?
}
(lhs @ (Value::Bytes(_) | Value::String(_)), Value::Null) => lhs,
(Value::String(lhs), Value::String(rhs)) => {
Value::String(Value::concat_strings(&lhs, &rhs))
}
(Value::Bytes(lhs), Value::Bytes(rhs)) => Value::concat_bytes(&lhs, &rhs).into(),
(Value::Bytes(lhs), Value::String(rhs)) => {
Value::concat_bytes_with_string(&lhs, &rhs).into()
}
(Value::String(lhs), Value::Bytes(rhs)) => {
Value::concat_string_with_bytes(&lhs, &rhs).into()
}
(Value::Null, rhs @ (Value::Bytes(_) | Value::String(_))) => rhs,
(lhs, rhs) => return Err(ValueError::Add(lhs.kind(), rhs.kind())),
};
Ok(value)
}
fn try_sub(self, rhs: Self) -> Result<Self, ValueError> {
let value = match (self, rhs) {
(Value::Integer(lhs), Value::Integer(rhs)) => i64::wrapping_sub(lhs, rhs).into(),
(Value::Integer(lhs), Value::Float(rhs)) => {
float_result(lhs as f64 - rhs.into_inner())?
}
(Value::Float(lhs), Value::Integer(rhs)) => {
float_result(lhs.into_inner() - rhs as f64)?
}
(Value::Float(lhs), Value::Float(rhs)) => {
float_result(lhs.into_inner() - rhs.into_inner())?
}
(lhs, rhs) => return Err(ValueError::Sub(lhs.kind(), rhs.kind())),
};
Ok(value)
}
fn try_or(
self,
mut rhs: impl FnMut() -> Result<Self, ExpressionError>,
) -> Result<Self, ValueError> {
let err = ValueError::Or;
match self {
Value::Null | Value::Boolean(false) => rhs().map_err(err),
value => Ok(value),
}
}
fn try_and(self, rhs: Self) -> Result<Self, ValueError> {
let value = match (self, rhs) {
(Value::Null, _) | (Value::Boolean(_), Value::Null) => false.into(),
(Value::Boolean(lhs), Value::Boolean(rhs)) => (lhs && rhs).into(),
(lhs, rhs) => return Err(ValueError::And(lhs.kind(), rhs.kind())),
};
Ok(value)
}
fn try_rem(self, rhs: Self) -> Result<Self, ValueError> {
let value = match (self, rhs) {
(_, Value::Integer(0)) => return Err(ValueError::DivideByZero),
(_, Value::Float(rhs)) if rhs.into_inner() == 0.0 => {
return Err(ValueError::DivideByZero);
}
(Value::Integer(lhs), Value::Integer(rhs)) => i64::wrapping_rem(lhs, rhs).into(),
(Value::Integer(lhs), Value::Float(rhs)) => {
float_result(lhs as f64 % rhs.into_inner())?
}
(Value::Float(lhs), Value::Integer(rhs)) => {
float_result(lhs.into_inner() % rhs as f64)?
}
(Value::Float(lhs), Value::Float(rhs)) => {
float_result(lhs.into_inner() % rhs.into_inner())?
}
(lhs, rhs) => return Err(ValueError::Rem(lhs.kind(), rhs.kind())),
};
Ok(value)
}
fn try_gt(self, rhs: Self) -> Result<Self, ValueError> {
let value = match (self, rhs) {
(Value::Integer(lhs), Value::Integer(rhs)) => (lhs > rhs).into(),
(Value::Integer(lhs), Value::Float(rhs)) => (lhs as f64 > rhs.into_inner()).into(),
(Value::Float(lhs), Value::Integer(rhs)) => (lhs.into_inner() > rhs as f64).into(),
(Value::Float(lhs), Value::Float(rhs)) => (lhs > rhs).into(),
(lhs @ (Value::Bytes(_) | Value::String(_)), rhs) => {
(lhs.try_bytes()? > rhs.try_bytes()?).into()
}
(Value::Timestamp(lhs), rhs) => (lhs > rhs.try_timestamp()?).into(),
(lhs, rhs) => return Err(ValueError::Rem(lhs.kind(), rhs.kind())),
};
Ok(value)
}
fn try_ge(self, rhs: Self) -> Result<Self, ValueError> {
let value = match (self, rhs) {
(Value::Integer(lhs), Value::Integer(rhs)) => (lhs >= rhs).into(),
(Value::Integer(lhs), Value::Float(rhs)) => (lhs as f64 >= rhs.into_inner()).into(),
(Value::Float(lhs), Value::Integer(rhs)) => (lhs.into_inner() >= rhs as f64).into(),
(Value::Float(lhs), Value::Float(rhs)) => (lhs >= rhs).into(),
(lhs @ (Value::Bytes(_) | Value::String(_)), rhs) => {
(lhs.try_bytes()? >= rhs.try_bytes()?).into()
}
(Value::Timestamp(lhs), rhs) => (lhs >= rhs.try_timestamp()?).into(),
(lhs, rhs) => return Err(ValueError::Ge(lhs.kind(), rhs.kind())),
};
Ok(value)
}
fn try_lt(self, rhs: Self) -> Result<Self, ValueError> {
let value = match (self, rhs) {
(Value::Integer(lhs), Value::Integer(rhs)) => (lhs < rhs).into(),
(Value::Integer(lhs), Value::Float(rhs)) => ((lhs as f64) < rhs.into_inner()).into(),
(Value::Float(lhs), Value::Integer(rhs)) => (lhs.into_inner() < rhs as f64).into(),
(Value::Float(lhs), Value::Float(rhs)) => (lhs < rhs).into(),
(lhs @ (Value::Bytes(_) | Value::String(_)), rhs) => {
(lhs.try_bytes()? < rhs.try_bytes()?).into()
}
(Value::Timestamp(lhs), rhs) => (lhs < rhs.try_timestamp()?).into(),
(lhs, rhs) => return Err(ValueError::Ge(lhs.kind(), rhs.kind())),
};
Ok(value)
}
fn try_le(self, rhs: Self) -> Result<Self, ValueError> {
let value = match (self, rhs) {
(Value::Integer(lhs), Value::Integer(rhs)) => (lhs <= rhs).into(),
(Value::Integer(lhs), Value::Float(rhs)) => (lhs as f64 <= rhs.into_inner()).into(),
(Value::Float(lhs), Value::Integer(rhs)) => (lhs.into_inner() <= rhs as f64).into(),
(Value::Float(lhs), Value::Float(rhs)) => (lhs <= rhs).into(),
(lhs @ (Value::Bytes(_) | Value::String(_)), rhs) => {
(lhs.try_bytes()? <= rhs.try_bytes()?).into()
}
(Value::Timestamp(lhs), rhs) => (lhs <= rhs.try_timestamp()?).into(),
(lhs, rhs) => return Err(ValueError::Ge(lhs.kind(), rhs.kind())),
};
Ok(value)
}
fn try_merge(self, rhs: Self) -> Result<Self, ValueError> {
match (self, rhs) {
(Value::Object(lhs), Value::Object(rhs)) => {
Ok(lhs.into_iter().chain(rhs).collect::<ObjectMap>().into())
}
(lhs, rhs) => Err(ValueError::Merge(lhs.kind(), rhs.kind())),
}
}
fn eq_lossy(&self, rhs: &Self) -> bool {
use Value::{Float, Integer};
match self {
Integer(lhv) => rhs.try_into_f64().is_ok_and(|rhv| *lhv as f64 == rhv),
Float(lhv) => rhs.try_into_f64().is_ok_and(|rhv| lhv.into_inner() == rhv),
_ => self == rhs,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn float(value: f64) -> Value {
Value::Float(NotNan::new(value).expect("test value must not be NaN"))
}
#[test]
fn float_arithmetic_returns_an_error_for_nan_results() {
assert_eq!(
float(f64::INFINITY).try_mul(float(0.0)),
Err(ValueError::NanFloat)
);
assert_eq!(
float(0.0).try_mul(float(f64::INFINITY)),
Err(ValueError::NanFloat)
);
assert_eq!(
Value::Integer(0).try_mul(float(f64::INFINITY)),
Err(ValueError::NanFloat)
);
assert_eq!(
float(f64::INFINITY).try_add(float(f64::NEG_INFINITY)),
Err(ValueError::NanFloat)
);
assert_eq!(
float(f64::NEG_INFINITY).try_add(float(f64::INFINITY)),
Err(ValueError::NanFloat)
);
assert_eq!(
float(f64::INFINITY).try_sub(float(f64::INFINITY)),
Err(ValueError::NanFloat)
);
assert_eq!(
float(f64::INFINITY).try_div(float(f64::INFINITY)),
Err(ValueError::NanFloat)
);
assert_eq!(
float(f64::INFINITY).try_rem(float(1.0)),
Err(ValueError::NanFloat)
);
}
#[test]
fn nan_arithmetic_errors_can_be_coalesced() {
let sources = [
r#"(parse_float!("inf") + parse_float!("-inf")) ?? 0"#,
r#"(parse_float!("-inf") + parse_float!("inf")) ?? 0"#,
r#"(parse_float!("inf") * 0) ?? 0"#,
r#"(0 * parse_float!("inf")) ?? 0"#,
r#"(parse_float!("inf") - parse_float!("inf")) ?? 0"#,
r#"(parse_float!("inf") / parse_float!("inf")) ?? 0"#,
r#"mod(parse_float!("inf"), 1.0) ?? 0"#,
];
for source in sources {
let compilation = crate::compiler::compile(source, &crate::stdlib::all())
.unwrap_or_else(|error| panic!("failed to compile `{source}`: {error:?}"));
let mut target = Value::Object(ObjectMap::new());
let result = crate::compiler::runtime::Runtime::default()
.resolve(
&mut target,
&compilation.program,
&crate::compiler::TimeZone::default(),
)
.unwrap_or_else(|error| panic!("failed to run `{source}`: {error}"));
assert_eq!(result, Value::Integer(0), "source: `{source}`");
}
}
#[test]
fn dynamic_float_arithmetic_preserves_existing_infallible_typing() {
let sources = [
(
"lhs = parse_float!(.lhs); rhs = parse_float!(.rhs); lhs + rhs",
float(3.0),
),
(
"lhs = parse_float!(.lhs); rhs = parse_float!(.rhs); lhs - rhs",
float(-1.0),
),
(
"lhs = parse_float!(.lhs); rhs = parse_float!(.rhs); lhs * rhs",
float(2.0),
),
("lhs = parse_float!(.lhs); lhs / 2", float(0.5)),
("lhs = parse_float!(.lhs); mod(lhs, 2.0)", float(1.0)),
];
let target = ObjectMap::from([
("lhs".into(), Value::from("1")),
("rhs".into(), Value::from("2")),
]);
for (source, expected) in sources {
let compilation = crate::compiler::compile(source, &crate::stdlib::all())
.unwrap_or_else(|error| panic!("failed to compile `{source}`: {error:?}"));
let mut target = Value::Object(target.clone());
let result = crate::compiler::runtime::Runtime::default()
.resolve(
&mut target,
&compilation.program,
&crate::compiler::TimeZone::default(),
)
.unwrap_or_else(|error| panic!("failed to run `{source}`: {error}"));
assert_eq!(result, expected, "source: `{source}`");
}
}
#[test]
fn dynamic_nan_arithmetic_returns_a_runtime_error() {
let source = "lhs = parse_float!(.lhs); rhs = parse_float!(.rhs); lhs + rhs";
let compilation = crate::compiler::compile(source, &crate::stdlib::all())
.unwrap_or_else(|error| panic!("failed to compile `{source}`: {error:?}"));
let mut target = Value::Object(ObjectMap::from([
("lhs".into(), Value::from("inf")),
("rhs".into(), Value::from("-inf")),
]));
let error = crate::compiler::runtime::Runtime::default()
.resolve(
&mut target,
&compilation.program,
&crate::compiler::TimeZone::default(),
)
.expect_err("NaN-producing arithmetic must fail");
assert!(error.to_string().contains("operation would produce NaN"));
}
#[test]
fn string_concat_and_repeat_preserve_variant() {
let concatenated = Value::from("a").try_add(Value::from("b")).unwrap();
assert!(matches!(concatenated, Value::String(_)));
assert_eq!(concatenated, Value::from("ab"));
let mixed = Value::from("a")
.try_add(Value::from_static_bytes(b"b"))
.unwrap();
assert!(matches!(mixed, Value::Bytes(_)));
assert_eq!(mixed, Value::from("ab"));
let with_null = Value::from("foo").try_add(Value::Null).unwrap();
assert!(matches!(with_null, Value::String(_)));
assert_eq!(with_null, Value::from("foo"));
let from_null = Value::Null.try_add(Value::from("foo")).unwrap();
assert!(matches!(from_null, Value::String(_)));
assert_eq!(from_null, Value::from("foo"));
let repeated = Value::from("ab").try_mul(Value::Integer(3)).unwrap();
assert!(matches!(repeated, Value::String(_)));
assert_eq!(repeated, Value::from("ababab"));
let repeated_bytes = Value::from_static_bytes(b"ab")
.try_mul(Value::Integer(2))
.unwrap();
assert!(matches!(repeated_bytes, Value::Bytes(_)));
assert_eq!(repeated_bytes, Value::from("abab"));
}
}