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

Allow clearing context variables for re-use #121

Closed
wants to merge 2 commits into from
Closed
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
11 changes: 11 additions & 0 deletions interpreter/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ impl Context<'_> {
}
}

pub fn clear_variables(&mut self) {
match self {
Context::Root { variables, .. } => {
variables.clear();
}
Context::Child { variables, .. } => {
variables.clear();
}
}
}

/// Constructs a new empty context with no variables or functions.
///
/// If you're looking for a context that has all the standard methods, functions
Expand Down
104 changes: 104 additions & 0 deletions interpreter/src/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,29 @@ pub enum Value {
Null,
}

impl Display for Value {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Value::List(l) => write!(
f,
"[{}]",
l.as_ref()
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(",")
),
Value::Int(i) => write!(f, "{}", i),
Value::UInt(u) => write!(f, "{}", u),
Value::Float(fl) => write!(f, "{}", fl),
Value::String(s) => write!(f, "{}", s),
Value::Bool(b) => write!(f, "{}", b),
Value::Null => write!(f, "null"),
o => write!(f, "{:?}", o),
}
}
}

#[derive(Clone, Copy, Debug)]
pub enum ValueType {
List,
Expand Down Expand Up @@ -417,6 +440,16 @@ impl Value {
#[inline(always)]
pub fn resolve(expr: &Expression, ctx: &Context) -> ResolveResult {
match expr {
Expression::TemplateLiteral(t) => {
let concatenated: Vec<String> = t
.iter()
.map(|e| {
let b = Value::resolve(e, ctx).unwrap();
b.to_string()
})
.collect();
Ok(Value::String(concatenated.join("").into()))
}
Expression::Atom(atom) => Ok(atom.into()),
Expression::Arithmetic(left, op, right) => {
let left = Value::resolve(left, ctx)?;
Expand Down Expand Up @@ -991,4 +1024,75 @@ mod tests {
"The AND expression should support short-circuit evaluation."
);
}

#[test]
fn test_template_string_basic() {
let mut context = Context::default();
context.add_variable_from_value("foo", "bar");
let program = Program::compile(r"`beep ${foo}`").unwrap();
let value = program.execute(&context);
assert!(value.is_ok());
assert_eq!(value.unwrap(), "beep bar".into());
}

#[test]
fn test_template_nested_path() {
let mut context = Context::default();
let mut map: HashMap<String, Value> = HashMap::new();
let mut nested: HashMap<String, Value> = HashMap::new();
nested.insert("baz".to_string(), "qux".into());
let nested: Value = Value::Map(nested.into());
map.insert("bar".to_string(), nested.into());
let map = Value::Map(map.into());
context.add_variable_from_value("foo", map);
let program = Program::compile(r"`beep ${foo.bar.baz}`").unwrap();
let value = program.execute(&context);
assert!(value.is_ok());
assert_eq!(value.unwrap(), "beep qux".into());
}

#[test]
fn test_negation() {
let mut context = Context::default();
context.add_variable_from_value("foo", Value::Int(5));
let program = Program::compile(r"`-5 == ${-foo}`").unwrap();
let value = program.execute(&context);
assert!(value.is_ok());
assert_eq!(value.unwrap(), "-5 == -5".into());
}

#[test]
fn test_array_access() {
let mut context = Context::default();
let arr = Value::List(Arc::new(
vec![Value::Int(1), Value::Int(2), Value::Int(3)].into(),
));
context.add_variable("foo", arr).unwrap();
let program = Program::compile(r"`${foo[0]}`").unwrap();
let value = program.execute(&context);
assert!(value.is_ok());
assert_eq!(value.unwrap(), "1".into());
}

#[test]
fn test_bool_eval() {
let mut context = Context::default();
let b = Value::Bool(true);
context.add_variable("foo", b).unwrap();
let program = Program::compile(r"`false != ${foo}`").unwrap();
let value = program.execute(&context);
assert!(value.is_ok());
assert_eq!(value.unwrap(), "false != true".into());
}

#[test]
fn test_null_eval() {
let mut context = Context::default();
let n = Value::Null;
context.add_variable("foo", n).unwrap();
let program = Program::compile(r"`I am ${foo}`").unwrap();
let value = program.execute(&context);
assert!(value.is_ok());
assert_eq!(value.unwrap(), "I am null".into());
}
}
4 changes: 4 additions & 0 deletions parser/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ pub enum Expression {

Atom(Atom),
Ident(Arc<String>),

TemplateLiteral(Vec<Expression>),
}


#[derive(Debug, PartialEq, Clone)]
pub enum Member {
Attribute(Arc<String>),
Expand Down Expand Up @@ -198,6 +201,7 @@ impl Expression {
Expression::Ident(v) => {
variables.insert(v.as_str());
}
Expression::TemplateLiteral(_) => {} // TODO:
}
}
}
Expand Down
73 changes: 71 additions & 2 deletions parser/src/cel.lalrpop
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{RelationOp, ArithmeticOp, Expression, UnaryOp, Member, Atom, parse_bytes, parse_string};
use crate::{RelationOp, ArithmeticOp, Expression, UnaryOp, Member, Atom, parse_bytes, parse_string, parse};
use std::sync::Arc;

grammar;
Expand Down Expand Up @@ -66,6 +66,7 @@ pub Primary: Expression = {
Expression::FunctionCall(Expression::Ident(identifier).into(), None, arguments).into()
},
Atom => Expression::Atom(<>).into(),
TemplateLiteral,
"[" <members:CommaSeparated<Expression>> "]" => Expression::List(<>).into(),
"{" <fields:CommaSeparated<MapInits>> "}" => Expression::Map(<>).into(),
"(" <Expression> ")"
Expand Down Expand Up @@ -119,6 +120,70 @@ RelationOp: RelationOp = {
"in" => RelationOp::In
}

TemplateLiteral: Expression = {
// Template literal - handle as a single regex pattern
// The pattern captures the entire template literal including expressions
r"`([^`\\$]*(?:\\.[^`\\$]*)*(?:\$\{(?:[^{}]|\{[^{}]*\})*\}[^`\\$]*)*)*`" => {
let template_str = <>;
let mut parts = Vec::new();
let mut current = String::new();
let mut chars = template_str[1..template_str.len()-1].chars().peekable();

while let Some(c) = chars.next() {
match c {
'\\' => {
if let Some(next) = chars.next() {
current.push(next);
}
},
'$' => {
if let Some('{') = chars.next() {
// Save current text if any
if !current.is_empty() {
parts.push(Expression::Atom(Atom::String(current.into())));
current = String::new();
}

// Collect expression string
let mut expr_str = String::new();
let mut brace_count = 1;

while brace_count > 0 {
if let Some(ch) = chars.next() {
match ch {
'{' => brace_count += 1,
'}' => brace_count -= 1,
_ => {}
}
if brace_count > 0 {
expr_str.push(ch);
}
}
}

// Parse expression using the grammar
match parse(&expr_str) {
Ok(expr) => {
parts.push(expr)
},
Err(_) => return Expression::Atom(Atom::String(Arc::new(template_str.to_string()))) // Fallback if expression parsing fails
}
} else {
current.push('$');
}
},
_ => current.push(c)
}
}

// Add any remaining text
if !current.is_empty() {
parts.push(Expression::Atom(Atom::String(current.into())));
}

Expression::TemplateLiteral(parts)
},
}
Atom: Atom = {
// Integer literals. Annoying to parse :/
r"-?[0-9]+" => Atom::Int(<>.parse().unwrap()),
Expand Down Expand Up @@ -162,11 +227,15 @@ Atom: Atom = {
r#"[bB]'(\\.|[^'\n])*'"# => Atom::Bytes(parse_bytes(&<>[2..<>.len()-1]).unwrap().into()),
// r#"[bB]'''(\\.|[^'{3}])*'''"# => Atom::Bytes(Vec::from(<>.as_bytes()).into()),


"true" => Atom::Bool(true),
"false" => Atom::Bool(false),
"null" => Atom::Null,
};


Ident: Arc<String> = {
r"[_a-zA-Z][_a-zA-Z0-9]*" => Arc::from(<>.to_string())
}
}