--- a/crates/jrsonnet-evaluator/build.rs +++ b/crates/jrsonnet-evaluator/build.rs @@ -35,7 +35,7 @@ Member::Field(FieldMember { name: FieldName::Fixed(name), .. - }) if **name == *"join" || **name == *"manifestJsonEx" || **name == *"escapeStringJson" + }) if **name == *"join" || **name == *"manifestJsonEx" || **name == *"escapeStringJson" || **name == *"equals" ) }) .collect(), --- a/crates/jrsonnet-evaluator/src/evaluate.rs +++ b/crates/jrsonnet-evaluator/src/evaluate.rs @@ -484,7 +484,14 @@ 0, a, vec![]; 1, b, vec![]; ], { - Val::Bool(a == b) + Val::Bool(primitive_equals(&a, &b)?) + }), + // faster + ("std", "equals") => parse_args!(context, "std.equals", args, 2, [ + 0, a, vec![]; + 1, b, vec![]; + ], { + Val::Bool(equals(&a, &b)?) }), ("std", "modulo") => parse_args!(context, "std.modulo", args, 2, [ 0, a: [Val::Num]!!Val::Num, vec![ValType::Num]; --- a/crates/jrsonnet-evaluator/src/lib.rs +++ b/crates/jrsonnet-evaluator/src/lib.rs @@ -380,7 +380,7 @@ #[cfg(test)] pub mod tests { use super::Val; - use crate::EvaluationState; + use crate::{create_error, EvaluationState, primitive_equals}; use jrsonnet_parser::*; use std::{path::PathBuf, rc::Rc}; @@ -411,11 +411,11 @@ fn eval_state_standard() { let state = EvaluationState::default(); state.with_stdlib(); - assert_eq!( - state - .parse_evaluate_raw(r#"std.assertEqual(std.base64("test"), "dGVzdA==")"#) - .unwrap(), - Val::Bool(true) + assert!( + primitive_equals( + &state.parse_evaluate_raw(r#"std.assertEqual(std.base64("test"), "dGVzdA==")"#).unwrap(), + &Val::Bool(true), + ).unwrap() ); } @@ -445,14 +445,14 @@ /// Asserts given code returns `true` macro_rules! assert_eval { ($str: expr) => { - assert_eq!(eval!($str), Val::Bool(true)) + assert!(primitive_equals(&eval!($str), &Val::Bool(true)).unwrap()) }; } /// Asserts given code returns `false` macro_rules! assert_eval_neg { ($str: expr) => { - assert_eq!(eval!($str), Val::Bool(false)) + assert!(primitive_equals(&eval!($str), &Val::Bool(false)).unwrap()) }; } macro_rules! assert_json { @@ -663,9 +663,11 @@ #[test] fn string_is_string() { - assert_eq!( - eval!("local arr = 'hello'; (!std.isArray(arr)) && (!std.isString(arr))"), - Val::Bool(false) + assert!( + primitive_equals( + &eval!("local arr = 'hello'; (!std.isArray(arr)) && (!std.isString(arr))"), + &Val::Bool(false), + ).unwrap() ); } @@ -767,4 +769,10 @@ ) }) } + + #[test] + fn equality(){ + println!("{:?}", jrsonnet_parser::parse("{ x: 1, y: 2 } == { x: 1, y: 2 }", &ParserSettings::default())); + assert_eval!("{ x: 1, y: 2 } == { x: 1, y: 2 }") + } } --- a/crates/jrsonnet-evaluator/src/val.rs +++ b/crates/jrsonnet-evaluator/src/val.rs @@ -111,7 +111,7 @@ } } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, Clone)] pub enum Val { Bool(bool), Null, @@ -212,6 +212,67 @@ } } +fn is_function_like(val: &Val) -> bool { + matches!(val, Val::Func(_) | Val::Intristic(_, _)) +} + +/// Implements std.primitiveEquals builtin +pub fn primitive_equals(val_a: &Val, val_b: &Val) -> Result { + Ok(match (val_a.unwrap_if_lazy()?, val_b.unwrap_if_lazy()?) { + (Val::Bool(a), Val::Bool(b)) => a == b, + (Val::Null, Val::Null) => true, + (Val::Str(a), Val::Str(b)) => a == b, + (Val::Num(a), Val::Num(b)) => (a - b).abs() <= f64::EPSILON, + (Val::Arr(_), Val::Arr(_)) => create_error_result(Error::RuntimeError( + "primitiveEquals operates on primitive types, got array".into(), + ))?, + (Val::Obj(_), Val::Obj(_)) => create_error_result(Error::RuntimeError( + "primitiveEquals operates on primitive types, got object".into(), + ))?, + (a, b) if is_function_like(&a) && is_function_like(&b) => create_error_result( + Error::RuntimeError("cannot test equality of functions".into()), + )?, + (_, _) => false, + }) +} + +/// Native implementation of std.equals +pub fn equals(val_a: &Val, val_b: &Val) -> Result { + let val_a = val_a.unwrap_if_lazy()?; + let val_b = val_b.unwrap_if_lazy()?; + + if val_a.value_type()? != val_b.value_type()? { + return Ok(false); + } + match (val_a, val_b) { + // Cant test for ptr equality, because all fields needs to be evaluated + (Val::Arr(a), Val::Arr(b)) => { + if a.len() != b.len() { + return Ok(false); + } + for (a, b) in a.iter().zip(b.iter()) { + if !equals(&a.unwrap_if_lazy()?, &b.unwrap_if_lazy()?)? { + return Ok(false); + } + } + Ok(true) + } + (Val::Obj(a), Val::Obj(b)) => { + let fields = a.visible_fields(); + if fields != b.visible_fields() { + return Ok(false); + } + for field in fields { + if !equals(&a.get(field.clone())?.unwrap(), &b.get(field)?.unwrap())? { + return Ok(false); + } + } + Ok(true) + } + (a, b) => Ok(primitive_equals(&a, &b)?), + } +} + pub fn manifest_json_ex(val: &Val, padding: &str) -> Result { let mut out = String::new(); manifest_json_ex_buf(val, &mut out, padding, &mut String::new())?;