Skip to content

Commit

Permalink
Add support for using a dynamic variable resolver and a Value::Dynami…
Browse files Browse the repository at this point in the history
…cCollection variant to the Value type

* includes a benchmark
* users can implement a DynamicCollection trait to aid in struct conversions to Values
* added pared feature which integrates the pared dependency so that you can use Arc projection refs to documents avoiding heavy clones when using a dynamic resolver.

Signed-off-by: Hiram Chirino <[email protected]>
  • Loading branch information
chirino committed Jul 16, 2024
1 parent 9a2dd72 commit 80a82dd
Show file tree
Hide file tree
Showing 6 changed files with 362 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
Cargo.lock
.idea
4 changes: 4 additions & 0 deletions interpreter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"] }
Expand Down
47 changes: 45 additions & 2 deletions interpreter/benches/runtime.rs
Original file line number Diff line number Diff line change
@@ -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![
Expand Down Expand Up @@ -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);
42 changes: 40 additions & 2 deletions interpreter/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,16 @@ pub enum Context<'a> {
Root {
functions: FunctionRegistry,
variables: HashMap<String, Value>,
dynamic_resolver: Option<DynamicResolverFn>,
},
Child {
parent: &'a Context<'a>,
variables: HashMap<String, Value>,
},
}

type DynamicResolverFn = Box<dyn Fn(&str) -> Option<Value> + Sync + Send>;

impl<'a> Context<'a> {
pub fn add_variable<S, V>(
&mut self,
Expand Down Expand Up @@ -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<S>(&self, name: S) -> Result<Value, ExecutionError>
where
S: Into<String>,
{
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<S>(&self, name: S) -> bool
where
S: Into<String>,
Expand Down Expand Up @@ -124,6 +140,27 @@ impl<'a> Context<'a> {
};
}

pub fn set_dynamic_resolver<F>(&mut self, handler: F)
where
F: Fn(&str) -> Option<Value> + Sync + Send + 'static,
{
if let Context::Root {
dynamic_resolver, ..
} = self
{
*dynamic_resolver = Some(Box::new(handler));
};
}

pub(crate) fn get_dynamic_resolver(&self) -> &Option<DynamicResolverFn> {
match self {
Context::Root {
dynamic_resolver, ..
} => dynamic_resolver,
Context::Child { parent, .. } => parent.get_dynamic_resolver(),
}
}

pub fn resolve(&self, expr: &Expression) -> Result<Value, ExecutionError> {
Value::resolve(expr, self)
}
Expand All @@ -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);
Expand Down
126 changes: 126 additions & 0 deletions interpreter/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,8 @@ fn _timestamp(i: &str) -> Result<DateTime<FixedOffset>> {

#[cfg(test)]
mod tests {
use std::sync::Arc;

use crate::context::Context;
use crate::testing::test_script;
use crate::{Program, Value};
Expand Down Expand Up @@ -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<String>,
homeworld: Option<String>,
}

impl ParcDynamicCollection<Species> for Species {
fn resolver(receiver: Parc<Species>) -> 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<String>,
species: Species,
}

impl ParcDynamicCollection<Character> for Character {
fn resolver(receiver: Parc<Character>) -> 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<Character>,
}

impl ParcDynamicCollection<Film> for Film {
fn resolver(receiver: Parc<Film>) -> 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())
);
}
}
Loading

0 comments on commit 80a82dd

Please sign in to comment.