diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index aab8450..f845cc3 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -9,6 +9,7 @@ mod debugging; pub mod numbers; pub mod sets; mod strings; +mod tracing; pub mod types; mod utils; @@ -56,7 +57,7 @@ lazy_static! { //rego::register(&mut m); //opa::register(&mut m); debugging::register(&mut m); - //tracing::register(&mut m); + tracing::register(&mut m); m }; diff --git a/src/builtins/tracing.rs b/src/builtins/tracing.rs new file mode 100644 index 0000000..7f68fb2 --- /dev/null +++ b/src/builtins/tracing.rs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::ast::Expr; +use crate::builtins; +use crate::builtins::utils::{ensure_args_count, ensure_string}; +use crate::lexer::Span; +use crate::value::Value; + +use std::collections::HashMap; + +use anyhow::Result; + +pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { + m.insert("trace", trace); +} + +// Symbol analyzer must ensure that vars used by trace are defined before +// the trace statement. Scheduler must ensure the above constraint. +fn trace(span: &Span, params: &[Expr], args: &[Value]) -> Result { + let name = "trace"; + ensure_args_count(span, name, params, args, 1)?; + let msg = ensure_string(name, ¶ms[0], &args[0])?; + + // Unlike rego, trace returns a string instead of bool. + // The interpreter accumulates the traces. + // TODO: Stateful bultins can pass in a state that would allow capturing + // the traces in the state. + Ok(Value::String(msg)) +} diff --git a/src/interpreter.rs b/src/interpreter.rs index 01687b5..b2f617d 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -31,6 +31,7 @@ pub struct Interpreter<'source> { active_rules: Vec<&'source Rule<'source>>, builtins_cache: BTreeMap<(&'static str, Vec), Value>, no_rules_lookup: bool, + traces: Option>, } #[derive(Debug, Clone)] @@ -66,6 +67,7 @@ impl<'source> Interpreter<'source> { active_rules: vec![], builtins_cache: BTreeMap::new(), no_rules_lookup: false, + traces: None, }) } @@ -1204,6 +1206,14 @@ impl<'source> Interpreter<'source> { } let v = builtin(span, ¶ms[..], &args[..])?; + + // Handle trace function. + // TODO: with modifier. + if let (Some(traces), Value::String(msg)) = (&mut self.traces, &v) { + traces.push(msg.clone()); + return Ok(Value::Bool(true)); + }; + if let Some(name) = cache { self.builtins_cache.insert((name, args), v.clone()); } @@ -1883,7 +1893,19 @@ impl<'source> Interpreter<'source> { } } - pub fn eval(&mut self, data: &Option, input: &Option) -> Result { + pub fn eval( + &mut self, + data: &Option, + input: &Option, + enable_tracing: bool, + ) -> Result { + self.traces = match enable_tracing { + true => Some(vec![]), + false => None, + }; + + self.builtins_cache.clear(); + if let Some(input) = input { self.input = input.clone(); @@ -1924,7 +1946,16 @@ impl<'source> Interpreter<'source> { Ok(self.data.clone()) } - pub fn eval_query_snippet(&mut self, snippet: &'source Expr<'source>) -> Result { + pub fn eval_query_snippet( + &mut self, + snippet: &'source Expr<'source>, + enable_tracing: bool, + ) -> Result { + self.traces = match enable_tracing { + true => Some(vec![]), + false => None, + }; + // Create a new scope for evaluating the expression. self.scopes.push(Scope::new()); let prev_module = self.set_current_module(self.modules.last().copied())?; diff --git a/tests/interpreter/cases/arithmetic/mod.rs b/tests/interpreter/cases/arithmetic/mod.rs index 8ec3fed..d9e1cb8 100644 --- a/tests/interpreter/cases/arithmetic/mod.rs +++ b/tests/interpreter/cases/arithmetic/mod.rs @@ -39,7 +39,7 @@ fn basic() -> Result<()> { )?; assert_eq!( - eval_file(&[rego.to_owned()], None, None, "data.test")?, + eval_file(&[rego.to_owned()], None, None, "data.test", false)?, expected ); Ok(()) diff --git a/tests/interpreter/cases/compr/mod.rs b/tests/interpreter/cases/compr/mod.rs index 818dcbc..bc07c03 100644 --- a/tests/interpreter/cases/compr/mod.rs +++ b/tests/interpreter/cases/compr/mod.rs @@ -47,7 +47,7 @@ fn basic_array() -> Result<()> { )?; assert_match( - eval_file(&[rego.to_owned()], None, None, "data.test")?, + eval_file(&[rego.to_owned()], None, None, "data.test", false)?, expected, ); Ok(()) @@ -116,7 +116,7 @@ fn basic_set() -> Result<()> { )?; assert_match( - eval_file(&[rego.to_owned()], None, None, "data.test")?, + eval_file(&[rego.to_owned()], None, None, "data.test", false)?, expected, ); Ok(()) diff --git a/tests/interpreter/cases/in/mod.rs b/tests/interpreter/cases/in/mod.rs index 5ac756e..02dd26f 100644 --- a/tests/interpreter/cases/in/mod.rs +++ b/tests/interpreter/cases/in/mod.rs @@ -120,7 +120,7 @@ fn basic() -> Result<()> { )?; assert_match( - eval_file(&[rego.to_owned()], None, None, "data.test")?, + eval_file(&[rego.to_owned()], None, None, "data.test", false)?, expected, ); Ok(()) diff --git a/tests/interpreter/cases/variables/mod.rs b/tests/interpreter/cases/variables/mod.rs index 3f0e924..6a00b88 100644 --- a/tests/interpreter/cases/variables/mod.rs +++ b/tests/interpreter/cases/variables/mod.rs @@ -47,7 +47,7 @@ fn basic() -> Result<()> { )?; assert_match( - eval_file(&[rego.to_owned()], None, None, "data.test")?, + eval_file(&[rego.to_owned()], None, None, "data.test", false)?, expected, ); Ok(()) diff --git a/tests/interpreter/mod.rs b/tests/interpreter/mod.rs index 99cd9f7..2f4dece 100644 --- a/tests/interpreter/mod.rs +++ b/tests/interpreter/mod.rs @@ -170,6 +170,7 @@ pub fn eval_file( data: Option, input: Option, query: &str, + enable_tracing: bool, ) -> Result { let mut files = vec![]; let mut sources = vec![]; @@ -199,7 +200,7 @@ pub fn eval_file( // First eval the modules. let mut interpreter = interpreter::Interpreter::new(modules_ref)?; - interpreter.eval(&data, &input)?; + interpreter.eval(&data, &input, enable_tracing)?; // Now eval the query. let source = Source { @@ -209,7 +210,7 @@ pub fn eval_file( }; let mut parser = Parser::new(&source)?; let expr = parser.parse_membership_expr()?; - interpreter.eval_query_snippet(&expr) + interpreter.eval_query_snippet(&expr, enable_tracing) } #[test] @@ -243,7 +244,7 @@ fn one_file() -> Result<()> { let mut parser = Parser::new(&source)?; let tree = parser.parse()?; let mut interpreter = interpreter::Interpreter::new(vec![&tree])?; - let results = interpreter.eval(&None, &input)?; + let results = interpreter.eval(&None, &input, true)?; println!("eval results:\n{}", serde_json::to_string_pretty(&results)?); Ok(()) } @@ -259,6 +260,7 @@ struct TestCase { want_result: Option, skip: Option, error: Option, + traces: Option, } #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -283,12 +285,14 @@ fn yaml_test_impl(file: &str) -> Result<()> { _ => panic!("either want_result or error must be specified in test case."), } + let enable_tracing = case.traces.is_some() && case.traces.unwrap(); // First eval the modules. match eval_file( &case.modules, Some(case.data), case.input, case.query.as_str(), + enable_tracing, ) { Ok(results) => match case.want_result { Some(want_result) => assert_match(results, want_result),