Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for using a dynamic variable resolver and a Value::DynamicCollection that can dynamically resolve nested values of collection types. #58

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
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
Loading