diff --git a/.gitignore b/.gitignore index 96ef6c0..408b8a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +.idea \ No newline at end of file diff --git a/interpreter/Cargo.toml b/interpreter/Cargo.toml index aec83bd..6e0b6ec 100644 --- a/interpreter/Cargo.toml +++ b/interpreter/Cargo.toml @@ -8,6 +8,9 @@ edition = "2021" license = "MIT" categories = ["compilers"] +[features] +pared = ["dep:pared"] + [dependencies] cel-parser = { path = "../parser", version = "0.7.0" } thiserror = "1.0.40" @@ -16,6 +19,7 @@ nom = "7.1.3" paste = "1.0.14" serde = "1.0.196" regex = "1.10.5" +pared = { version="0.3.0", optional = true } [dev-dependencies] criterion = { version = "0.5.1", features = ["html_reports"] } diff --git a/interpreter/benches/runtime.rs b/interpreter/benches/runtime.rs index 8289fb6..67830f1 100644 --- a/interpreter/benches/runtime.rs +++ b/interpreter/benches/runtime.rs @@ -1,7 +1,8 @@ use cel_interpreter::context::Context; -use cel_interpreter::Program; +use cel_interpreter::{Program, Value}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use std::collections::HashMap; +use std::sync::Arc; pub fn criterion_benchmark(c: &mut Criterion) { let expressions = vec![ @@ -66,5 +67,47 @@ pub fn map_macro_benchmark(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, criterion_benchmark, map_macro_benchmark); +pub fn variable_resolution_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("variable resolution"); + let sizes = vec![1, 10, 100]; + + // flip this bool to compare the performance of dynamic resolver vs static resolvers + let use_dynamic_resolver = true; + + for size in sizes { + let mut expr = String::new(); + + let mut doc = HashMap::new(); + for i in 0..size { + doc.insert(format!("var_{i}", i = i), Value::Null); + expr.push_str(&format!("var_{i}", i = i)); + if i < size - 1 { + expr.push_str("||"); + } + } + + let doc = Arc::new(doc); + let program = Program::compile(&expr).unwrap(); + group.bench_function(format!("variable_resolution_{}", size).as_str(), |b| { + let mut ctx = Context::default(); + if use_dynamic_resolver { + let doc = doc.clone(); + ctx.set_dynamic_resolver(move |var| doc.get(var).cloned()); + } else { + doc.iter() + .for_each(|(k, v)| ctx.add_variable_from_value(k.to_string(), v.clone())); + } + b.iter(|| program.execute(&ctx).unwrap()) + }); + } + group.finish(); +} + +criterion_group!( + benches, + criterion_benchmark, + map_macro_benchmark, + variable_resolution_benchmark +); + criterion_main!(benches); diff --git a/interpreter/src/context.rs b/interpreter/src/context.rs index 5a48c47..b9e7df8 100644 --- a/interpreter/src/context.rs +++ b/interpreter/src/context.rs @@ -32,6 +32,7 @@ pub enum Context<'a> { Root { functions: FunctionRegistry, variables: HashMap, + dynamic_resolver: Option, }, Child { parent: &'a Context<'a>, @@ -39,6 +40,8 @@ pub enum Context<'a> { }, } +type DynamicResolverFn = Box Option + Sync + Send>; + impl<'a> Context<'a> { pub fn add_variable( &mut self, @@ -85,14 +88,27 @@ impl<'a> Context<'a> { .get(&name) .cloned() .or_else(|| parent.get_variable(&name).ok()) - .ok_or_else(|| ExecutionError::UndeclaredReference(name.into())), + .map_or_else(|| self.get_dynamic_variable(name), Ok), Context::Root { variables, .. } => variables .get(&name) .cloned() - .ok_or_else(|| ExecutionError::UndeclaredReference(name.into())), + .map_or_else(|| self.get_dynamic_variable(name), Ok), } } + pub fn get_dynamic_variable(&self, name: S) -> Result + where + S: Into, + { + let name = name.into(); + return if let Some(dynamic_resolver) = self.get_dynamic_resolver() { + (dynamic_resolver)(name.as_str()) + .ok_or_else(|| ExecutionError::UndeclaredReference(name.into())) + } else { + Err(ExecutionError::UndeclaredReference(name.into())) + }; + } + pub(crate) fn has_function(&self, name: S) -> bool where S: Into, @@ -124,6 +140,27 @@ impl<'a> Context<'a> { }; } + pub fn set_dynamic_resolver(&mut self, handler: F) + where + F: Fn(&str) -> Option + Sync + Send + 'static, + { + if let Context::Root { + dynamic_resolver, .. + } = self + { + *dynamic_resolver = Some(Box::new(handler)); + }; + } + + pub(crate) fn get_dynamic_resolver(&self) -> &Option { + match self { + Context::Root { + dynamic_resolver, .. + } => dynamic_resolver, + Context::Child { parent, .. } => parent.get_dynamic_resolver(), + } + } + pub fn resolve(&self, expr: &Expression) -> Result { Value::resolve(expr, self) } @@ -145,6 +182,7 @@ impl<'a> Default for Context<'a> { let mut ctx = Context::Root { variables: Default::default(), functions: Default::default(), + dynamic_resolver: None, }; ctx.add_function("contains", functions::contains); ctx.add_function("size", functions::size); diff --git a/interpreter/src/functions.rs b/interpreter/src/functions.rs index 87dc470..cfe0de1 100644 --- a/interpreter/src/functions.rs +++ b/interpreter/src/functions.rs @@ -548,6 +548,8 @@ fn _timestamp(i: &str) -> Result> { #[cfg(test)] mod tests { + use std::sync::Arc; + use crate::context::Context; use crate::testing::test_script; use crate::{Program, Value}; @@ -798,4 +800,128 @@ mod tests { .iter() .for_each(assert_script); } + + #[test] + fn test_dynamic_resolver() { + // You can resolve dynamic values by providing a custom resolver function. + let external_values = Arc::new(HashMap::from([("hello".to_string(), "world".to_string())])); + + let mut ctx = Context::default(); + ctx.set_dynamic_resolver(move |ident| { + external_values + .get(ident) + .map(|v| Value::String(v.clone().into())) + }); + assert_eq!(test_script("hello == 'world'", Some(ctx)), Ok(true.into())); + } + + #[cfg(feature = "pared")] + #[test] + fn test_deep_dynamic_resolver() { + use crate::objects::{MemberResolver, ParcDynamicCollection}; + use pared::sync::Parc; + + #[derive(Clone)] + struct Species { + name: String, + language: Option, + homeworld: Option, + } + + impl ParcDynamicCollection for Species { + fn resolver(receiver: Parc) -> MemberResolver { + MemberResolver::attribute_handler(move |attribute| match attribute.as_str() { + "name" => Some(receiver.name.clone().into()), + "language" => Some(receiver.language.clone().into()), + "homeworld" => Some(receiver.homeworld.clone().into()), + _ => None, + }) + } + } + + #[derive(Clone)] + struct Character { + name: String, + gender: Option, + species: Species, + } + + impl ParcDynamicCollection for Character { + fn resolver(receiver: Parc) -> MemberResolver { + MemberResolver::attribute_handler(move |attribute| match attribute.as_str() { + "name" => Some(receiver.name.clone().into()), + "gender" => Some(receiver.gender.clone().into()), + "species" => Some(receiver.project(|receiver| &receiver.species).into()), + _ => None, + }) + } + } + + #[derive(Clone)] + struct Film { + director: String, + title: String, + characters: Vec, + } + + impl ParcDynamicCollection for Film { + fn resolver(receiver: Parc) -> MemberResolver { + MemberResolver::attribute_handler(move |attribute| match attribute.as_str() { + "director" => Some(receiver.director.clone().into()), + "title" => Some(receiver.title.clone().into()), + "characters" => Some(receiver.project(|receiver| &receiver.characters).into()), + _ => None, + }) + } + } + + let doc = Parc::new(Film { + director: "George Lucas".to_string(), + title: "A New Hope".to_string(), + characters: vec![ + Character { + name: "Luke Skywalker".to_string(), + gender: Some("male".to_string()), + species: Species { + name: "Human".to_string(), + language: Some("english".to_string()), + homeworld: Some("Earth".to_string()), + }, + }, + Character { + name: "C-3PO".to_string(), + gender: None, + species: Species { + name: "Droid".to_string(), + language: None, + homeworld: None, + }, + }, + Character { + name: "Chewbacca".to_string(), + gender: Some("male".to_string()), + species: Species { + name: "Wookie".to_string(), + language: None, + homeworld: Some("Kashyyyk".to_string()), + }, + }, + ], + }); + + let mut ctx = Context::default(); + ctx.set_dynamic_resolver(move |name| match name { + "film" => Some(doc.clone().into()), + _ => None, + }); + assert_eq!( + test_script( + "film.title == 'A New Hope' && \ + film.characters[0].name =='Luke Skywalker' && \ + film.characters[0].species.name == 'Human'", + Some(ctx), + ), + Ok(true.into()) + ); + } } diff --git a/interpreter/src/objects.rs b/interpreter/src/objects.rs index 83c9aad..69b3533 100644 --- a/interpreter/src/objects.rs +++ b/interpreter/src/objects.rs @@ -10,6 +10,7 @@ use serde::{Serialize, Serializer}; use std::cmp::Ordering; use std::collections::HashMap; use std::convert::{Infallible, TryFrom, TryInto}; +use std::fmt; use std::fmt::{Display, Formatter}; use std::sync::Arc; @@ -149,8 +150,63 @@ impl TryIntoValue for Value { } } +#[derive(Clone, Debug)] +pub enum ResolvedMember { + Index(Value), + Attribute(Arc), +} + +pub type DynamicCollectionResolverFn = Box Option + Sync + Send>; + +// Value's have to implement Debug but Fn's don't, so we +// create a wrapper that we can implement Debug for. +#[derive(Clone)] +pub struct MemberResolver { + pub(crate) resolver: Arc, +} + +impl MemberResolver { + pub fn new(handler: F) -> Self + where + F: Fn(ResolvedMember) -> Option + Sync + Send + 'static, + { + MemberResolver { + resolver: Arc::new(Box::new(handler)), + } + } + + pub fn attribute_handler(handler: F) -> Self + where + F: Fn(Arc) -> Option + Sync + Send + 'static, + { + MemberResolver::new(move |member| match member { + ResolvedMember::Attribute(name) => handler(name), + _ => None, + }) + } + + pub fn index_handler(handler: F) -> Self + where + F: Fn(Value) -> Option + Sync + Send + 'static, + { + MemberResolver::new(move |member| match member { + ResolvedMember::Index(i) => handler(i), + _ => None, + }) + } +} + +impl fmt::Debug for MemberResolver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DynamicCollection").finish() + } +} + #[derive(Debug, Clone)] pub enum Value { + // DynamicCollection is a special type that allows for dynamic resolution of members. + DynamicCollection(MemberResolver), + List(Arc>), Map(Map), @@ -181,6 +237,7 @@ pub enum ValueType { Duration, Timestamp, Null, + DynamicCollection, } impl Display for ValueType { @@ -198,6 +255,7 @@ impl Display for ValueType { ValueType::Duration => write!(f, "duration"), ValueType::Timestamp => write!(f, "timestamp"), ValueType::Null => write!(f, "null"), + ValueType::DynamicCollection => write!(f, "dynamic_collection"), } } } @@ -217,6 +275,7 @@ impl Value { Value::Duration(_) => ValueType::Duration, Value::Timestamp(_) => ValueType::Timestamp, Value::Null => ValueType::Null, + Value::DynamicCollection(_) => ValueType::DynamicCollection, } } @@ -560,6 +619,11 @@ impl<'a> Value { Member::Index(idx) => { let idx = Value::resolve(idx, ctx)?; match (self, idx) { + (Value::DynamicCollection(dc), idx) => { + (dc.resolver)(ResolvedMember::Index(idx)) + .unwrap_or(Value::Null) + .into() + } (Value::List(items), Value::Int(idx)) => { items.get(idx as usize).unwrap().clone().into() } @@ -598,6 +662,9 @@ impl<'a> Value { // This will always either be because we're trying to access // a property on self, or a method on self. let child = match self { + Value::DynamicCollection(ref dc) => { + (dc.resolver)(ResolvedMember::Attribute(name.clone())) + } Value::Map(ref m) => m.map.get(&name.clone().into()).cloned(), _ => None, }; @@ -629,6 +696,7 @@ impl<'a> Value { Value::Duration(v) => v.num_nanoseconds().map(|n| n != 0).unwrap_or(false), Value::Timestamp(v) => v.timestamp_nanos_opt().unwrap_or_default() > 0, Value::Function(_, _) => false, + Value::DynamicCollection(_) => false, } } } @@ -648,6 +716,84 @@ impl From<&Atom> for Value { } } +// implementing DynamicCollection makes it easier to covert a user struct to a Value +pub trait DynamicCollection { + fn resolver(&self) -> MemberResolver; +} + +impl From for Value +where + T: DynamicCollection, +{ + fn from(item: T) -> Self { + Value::DynamicCollection(item.resolver()) + } +} + +impl From> for Value +where + T: DynamicCollection, +{ + fn from(item: Arc) -> Self { + Value::DynamicCollection(item.resolver()) + } +} + +#[cfg(feature = "pared")] +pub trait ParcDynamicCollection { + fn resolver(item: pared::sync::Parc) -> MemberResolver; +} + +#[cfg(feature = "pared")] +impl From> for Value +where + T: ParcDynamicCollection, +{ + fn from(item: pared::sync::Parc) -> Self { + Value::DynamicCollection(T::resolver(item)) + } +} + +#[cfg(feature = "pared")] +impl From>> for Value +where + T: ParcDynamicCollection + Send + Sync + 'static, +{ + fn from(items: pared::sync::Parc>) -> Self { + let items = items.clone(); + Value::DynamicCollection(MemberResolver::new(move |member| { + let items = items.clone(); + match member { + ResolvedMember::Index(Value::Int(idx)) => items + .try_project(|receiver| receiver.get(idx as usize).ok_or(())) + .ok() + .map(|x| x.into()), + _ => None, + } + })) + } +} + +#[cfg(feature = "pared")] +impl From>> for Value +where + T: ParcDynamicCollection + Send + Sync + 'static, +{ + fn from(items: pared::sync::Parc>) -> Self { + let items = items.clone(); + Value::DynamicCollection(MemberResolver::new(move |member| { + let items = items.clone(); + match member { + ResolvedMember::Attribute(name) => items + .try_project(|items| items.get(name.as_ref()).ok_or(())) + .ok() + .map(|x| x.into()), + _ => None, + } + })) + } +} + impl ops::Add for Value { type Output = Value;