From a3a20a1235cb5a377b336a5e6e29486981138fca Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:08:37 -0600 Subject: [PATCH] feat!: Rego -> RVM Compiler and extensive testsuite (#506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # RVM compiler test cases Coverage: - arithmetic - arrays - chained lookups - comparisons - comprehensions - default rules - destructuring - function rules - loops/quantifiers - multiple entrypoints - objects/sets - variables - negative/edge scenarios such as data/rule conflicts - virtual data lookups - etc # Modify interpreter and compiled policy for RVM Compilation - Interpreter::eval_default_rule_for_compiler: evaluates a named default rule in isolation - allows compiler to emit a constant value instead of instructions for the default value # feat: Rego Compiler Scaffolding - Introduce the rego::compiler module surface and entry point wiring - Add the core compiler concepts: - register allocator - scope tracking - literal/builtin tables - rule worklists - instruction emit helpers - compiler-specific error types - context structs for rules, comprehensions, and loops to support later lowering passes. # feat: Compile Rules/Queries - add compiler::compile_from_policy workflow plus rule worklist, entry-point wiring, and recursion checks - implement query lowering: - scheduling-aware statement ordering - loop hoisting - โ€œevery/someโ€ semantics - context yields - literal assertions - finalize Program construction # feat: Expression Lowering - add compile_rego_expr and helpers to translate every AST expression into RVM instructions, - interop with binding plans, comprehensions, and membership checks. - implement collection literal builders (ArrayCreate, SetCreate, ObjectCreate) - dedupe literal keys and handle mixed literal/dynamic fields via instruction data blocks. - operations: - arithmetic/boolean/bin operators - membership - unary minus - set unions/intersections - etc - user-defined and builtin function calls - reference handling - analyse chained refs - distinguishe data/input/local roots - perform rule dispatch or virtual document lookups - emits optimized Index/ChainedIndex instructions. # feat: Comprehensions & Loops - shared comprehension emitter - wraps array/set/object comprehensions with ComprehensionBegin/End - context management - loop lowering utilities - read hoisting metadata - emit LoopStart/LoopNext - some in lowering - every quantifiers - index iteration - propagate binding plans into stored registers so downstream statements see bound variables. # feat: Destructuring Lowering - destructuring planner integration - assignment/parameter/loop bindings use hoisted plans instead of re-walking ASTs. - handle :=, =, wildcard matches, and equality - evaluate RHS - applying destructuring plans - emit assert condition as needed - support nested array/object destructuring, dynamic keys, and some ... in forms # test: Shared Testing + RVM Suites - move YAML test helpers into test_utils.rs and re-export via common.rs for use by interpreter and vm test suites - comprehensive compiler test suite - compiles policies with the new Regoโ†’RVM compiler - runs them through RegoVM - compares against interpreter behavior - supports multiple entry points - provides assembly listings - filterable YAML suites. Signed-off-by: Anand Krishnamoorthi --- src/compiled_policy.rs | 17 +- src/interpreter.rs | 33 + src/languages/rego/compiler/comprehensions.rs | 112 ++++ src/languages/rego/compiler/core.rs | 256 ++++++++ src/languages/rego/compiler/destructuring.rs | 363 +++++++++++ src/languages/rego/compiler/error.rs | 71 +++ src/languages/rego/compiler/expressions.rs | 121 ++++ .../expressions/collection_literals.rs | 130 ++++ .../rego/compiler/expressions/operations.rs | 248 ++++++++ src/languages/rego/compiler/function_calls.rs | 76 +++ src/languages/rego/compiler/loops.rs | 406 ++++++++++++ src/languages/rego/compiler/mod.rs | 162 +++++ src/languages/rego/compiler/program.rs | 161 +++++ src/languages/rego/compiler/queries.rs | 245 ++++++++ src/languages/rego/compiler/references.rs | 431 +++++++++++++ src/languages/rego/compiler/rules.rs | 529 ++++++++++++++++ src/languages/rego/mod.rs | 2 + src/lib.rs | 5 + src/rvm/tests/vm.rs | 2 +- src/test_utils.rs | 127 ++++ src/tests/common.rs | 188 ++++++ src/tests/interpreter/mod.rs | 139 +---- tests/mod.rs | 3 + tests/rvm/mod.rs | 3 + tests/rvm/rego/cases/arithmetic.yaml | 50 ++ tests/rvm/rego/cases/arrays.yaml | 64 ++ tests/rvm/rego/cases/chained_access.yaml | 293 +++++++++ tests/rvm/rego/cases/comparisons.yaml | 50 ++ tests/rvm/rego/cases/comprehensions.yaml | 15 + tests/rvm/rego/cases/default_rules.yaml | 232 +++++++ tests/rvm/rego/cases/destructuring.yaml | 260 ++++++++ tests/rvm/rego/cases/examples.yaml | 72 +++ tests/rvm/rego/cases/function_rules.yaml | 230 +++++++ .../rvm/rego/cases/local_chained_access.yaml | 322 ++++++++++ .../rvm/rego/cases/loops_and_quantifiers.yaml | 89 +++ .../rvm/rego/cases/multiple_entry_points.yaml | 78 +++ tests/rvm/rego/cases/objects.yaml | 108 ++++ tests/rvm/rego/cases/rule_data_conflicts.yaml | 145 +++++ tests/rvm/rego/cases/set_rules.yaml | 153 +++++ tests/rvm/rego/cases/sets.yaml | 69 +++ tests/rvm/rego/cases/variables_and_rules.yaml | 50 ++ .../cases/virtual_data_document_lookup.yaml | 396 ++++++++++++ tests/rvm/rego/mod.rs | 578 ++++++++++++++++++ 43 files changed, 6945 insertions(+), 139 deletions(-) create mode 100644 src/languages/rego/compiler/comprehensions.rs create mode 100644 src/languages/rego/compiler/core.rs create mode 100644 src/languages/rego/compiler/destructuring.rs create mode 100644 src/languages/rego/compiler/error.rs create mode 100644 src/languages/rego/compiler/expressions.rs create mode 100644 src/languages/rego/compiler/expressions/collection_literals.rs create mode 100644 src/languages/rego/compiler/expressions/operations.rs create mode 100644 src/languages/rego/compiler/function_calls.rs create mode 100644 src/languages/rego/compiler/loops.rs create mode 100644 src/languages/rego/compiler/mod.rs create mode 100644 src/languages/rego/compiler/program.rs create mode 100644 src/languages/rego/compiler/queries.rs create mode 100644 src/languages/rego/compiler/references.rs create mode 100644 src/languages/rego/compiler/rules.rs create mode 100644 src/languages/rego/mod.rs create mode 100644 src/test_utils.rs create mode 100644 src/tests/common.rs create mode 100644 tests/rvm/mod.rs create mode 100644 tests/rvm/rego/cases/arithmetic.yaml create mode 100644 tests/rvm/rego/cases/arrays.yaml create mode 100644 tests/rvm/rego/cases/chained_access.yaml create mode 100644 tests/rvm/rego/cases/comparisons.yaml create mode 100644 tests/rvm/rego/cases/comprehensions.yaml create mode 100644 tests/rvm/rego/cases/default_rules.yaml create mode 100644 tests/rvm/rego/cases/destructuring.yaml create mode 100644 tests/rvm/rego/cases/examples.yaml create mode 100644 tests/rvm/rego/cases/function_rules.yaml create mode 100644 tests/rvm/rego/cases/local_chained_access.yaml create mode 100644 tests/rvm/rego/cases/loops_and_quantifiers.yaml create mode 100644 tests/rvm/rego/cases/multiple_entry_points.yaml create mode 100644 tests/rvm/rego/cases/objects.yaml create mode 100644 tests/rvm/rego/cases/rule_data_conflicts.yaml create mode 100644 tests/rvm/rego/cases/set_rules.yaml create mode 100644 tests/rvm/rego/cases/sets.yaml create mode 100644 tests/rvm/rego/cases/variables_and_rules.yaml create mode 100644 tests/rvm/rego/cases/virtual_data_document_lookup.yaml create mode 100644 tests/rvm/rego/mod.rs diff --git a/src/compiled_policy.rs b/src/compiled_policy.rs index 6a9db74..6e1214d 100644 --- a/src/compiled_policy.rs +++ b/src/compiled_policy.rs @@ -25,7 +25,7 @@ pub(crate) type InferredResourceTypes = BTreeMap, ResourceTypeInfo>; /// Wrapper around CompiledPolicyData that holds an Rc reference. #[derive(Debug, Clone)] pub struct CompiledPolicy { - inner: Rc, + pub(crate) inner: Rc, } impl CompiledPolicy { @@ -33,6 +33,21 @@ impl CompiledPolicy { pub(crate) fn new(inner: Rc) -> Self { Self { inner } } + + /// Get access to the rules in the compiled policy for downstream consumers like the RVM compiler. + pub fn get_rules(&self) -> &Map>> { + &self.inner.rules + } + + /// Get access to the modules in the compiled policy. + pub fn get_modules(&self) -> &Vec> { + self.inner.modules.as_ref() + } + + /// Returns true when the compiled policy should use Rego v0 semantics. + pub fn is_rego_v0(&self) -> bool { + !self.inner.modules.iter().any(|module| module.rego_v1) + } } impl CompiledPolicy { diff --git a/src/interpreter.rs b/src/interpreter.rs index 5a8f0a7..7dc1375 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -3371,6 +3371,39 @@ impl Interpreter { Ok(()) } + /// Evaluate a default rule and return the resulting value for compiler consumers. + pub fn eval_default_rule_for_compiler(&mut self, rule_path: &str) -> Result { + self.input = Value::Undefined; + self.data = Value::Undefined; + + let default_rules = self.compiled_policy.default_rules.get(rule_path).cloned(); + + if let Some(rules) = default_rules { + for (rule, _) in rules { + for module in self.compiled_policy.modules.iter() { + if module.policy.contains(&rule) { + let prev_module = self.set_current_module(Some(module.clone()))?; + let result = self.eval_default_rule(&rule); + self.set_current_module(prev_module)?; + + if result.is_ok() { + let components: Vec<&str> = rule_path.split('.').skip(1).collect(); + let value = Self::get_value_chained(self.data.clone(), &components); + + if value != Value::Undefined { + return Ok(value); + } + } + + return result.map(|_| Value::Undefined); + } + } + } + } + + bail!("Could not find default rule for path: {}", rule_path); + } + fn update_data( &mut self, span: &Span, diff --git a/src/languages/rego/compiler/comprehensions.rs b/src/languages/rego/compiler/comprehensions.rs new file mode 100644 index 0000000..cbb3063 --- /dev/null +++ b/src/languages/rego/compiler/comprehensions.rs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{CompilationContext, Compiler, ComprehensionType, ContextType, Register, Result}; +use crate::ast::{ExprRef, Query}; +use crate::lexer::Span; +use crate::rvm::instructions::{ComprehensionBeginParams, ComprehensionMode}; +use crate::rvm::Instruction; + +impl<'a> Compiler<'a> { + fn compile_comprehension( + &mut self, + mode: ComprehensionMode, + context_type: ComprehensionType, + key_expr: Option<&ExprRef>, + value_expr: Option<&ExprRef>, + query: &Query, + span: &Span, + ) -> Result { + let result_reg = self.alloc_register(); + let key_reg = self.alloc_register(); + let value_reg = self.alloc_register(); + + let params_index = self + .program + .add_comprehension_begin_params(ComprehensionBeginParams { + mode, + collection_reg: result_reg, + result_reg, + key_reg, + value_reg, + body_start: 0, + comprehension_end: 0, + }); + + self.emit_instruction(Instruction::ComprehensionBegin { params_index }, span); + + let body_start = self.program.instructions.len() as u16; + + let context = CompilationContext { + context_type: ContextType::Comprehension(context_type), + dest_register: result_reg, + key_expr: key_expr.cloned(), + value_expr: value_expr.cloned(), + span: span.clone(), + key_value_loops_hoisted: false, + }; + self.push_context(context); + self.compile_query(query)?; + self.pop_context(); + + self.emit_instruction(Instruction::ComprehensionEnd {}, span); + let comprehension_end = self.program.instructions.len() as u16; + + self.program + .update_comprehension_begin_params(params_index, |params| { + params.body_start = body_start; + params.comprehension_end = comprehension_end; + }); + + Ok(result_reg) + } + + pub(super) fn compile_array_comprehension( + &mut self, + term: &ExprRef, + query: &Query, + span: &Span, + ) -> Result { + self.compile_comprehension( + ComprehensionMode::Array, + ComprehensionType::Array, + None, + Some(term), + query, + span, + ) + } + + pub(super) fn compile_set_comprehension( + &mut self, + term: &ExprRef, + query: &Query, + span: &Span, + ) -> Result { + self.compile_comprehension( + ComprehensionMode::Set, + ComprehensionType::Set, + None, + Some(term), + query, + span, + ) + } + + pub(super) fn compile_object_comprehension( + &mut self, + key: &ExprRef, + value: &ExprRef, + query: &Query, + span: &Span, + ) -> Result { + self.compile_comprehension( + ComprehensionMode::Object, + ComprehensionType::Object, + Some(key), + Some(value), + query, + span, + ) + } +} diff --git a/src/languages/rego/compiler/core.rs b/src/languages/rego/compiler/core.rs new file mode 100644 index 0000000..e9c1c32 --- /dev/null +++ b/src/languages/rego/compiler/core.rs @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{CompilationContext, Compiler, CompilerError, Register, Result, Scope}; +use crate::ast::ExprRef; +use crate::builtins; +use crate::compiler::destructuring_planner::plans::BindingPlan; +use crate::lexer::Span; +use crate::rvm::program::{BuiltinInfo, SpanInfo}; +use crate::rvm::Instruction; +use crate::Value; +use alloc::format; +use alloc::string::{String, ToString}; + +impl<'a> Compiler<'a> { + /// Check if a function path is a builtin function (similar to interpreter's is_builtin) + pub(super) fn is_builtin(&self, path: &str) -> bool { + path == "print" || builtins::BUILTINS.contains_key(path) + } + + /// Check if a function path is a user-defined function rule + pub(super) fn is_user_defined_function(&self, rule_path: &str) -> bool { + self.policy.inner.rules.contains_key(rule_path) + } + + /// Get builtin index for a builtin function + pub(super) fn get_builtin_index(&mut self, builtin_name: &str) -> Result { + if !self.is_builtin(builtin_name) { + return Err(CompilerError::NotBuiltinFunction { + name: builtin_name.to_string(), + }); + } + + // Check if we already have an index for this builtin + if let Some(&index) = self.builtin_index_map.get(builtin_name) { + return Ok(index); + } + + // Get the builtin function info to determine number of arguments + let num_args = if builtin_name == "print" { + 2 // Special case for print + } else if let Some(builtin_fcn) = builtins::BUILTINS.get(builtin_name) { + builtin_fcn.1 as u16 // Second element is the number of arguments + } else { + return Err(CompilerError::UnknownBuiltinFunction { + name: builtin_name.to_string(), + }); + }; + + // Create builtin info and add it to the program + let builtin_info = BuiltinInfo { + name: builtin_name.to_string(), + num_args, + }; + let index = self.program.add_builtin_info(builtin_info); + + // Store in our mapping + self.builtin_index_map + .insert(builtin_name.to_string(), index); + + Ok(index) + } + + pub fn alloc_register(&mut self) -> Register { + // Assert that we don't exceed 256 registers (u8::MAX + 1) + assert!( + self.register_counter < 255, + "Register overflow: attempted to allocate register {}, but maximum is 255. \ + Consider using register windowing or spill handling.", + self.register_counter + ); + + let reg = self.register_counter; + self.register_counter += 1; + + reg + } + + /// Add a literal value to the literal table, returning its index + pub fn add_literal(&mut self, value: Value) -> u16 { + // Check if literal already exists to avoid duplication + // TODO: Optimize lookup + for (idx, existing) in self.program.literals.iter().enumerate() { + if existing == &value { + return idx as u16; + } + } + + let idx = self.program.literals.len() as u16; + self.program.literals.push(value); + idx + } + + /// Push a new variable scope (like the interpreter) + pub fn push_scope(&mut self) { + self.scopes.push(Scope::default()); + } + + /// Pop the current variable scope (like the interpreter) + pub fn pop_scope(&mut self) { + if self.scopes.len() > 1 { + self.scopes.pop(); + } + } + + /// Reset input/data registers for a new rule definition + /// This ensures input and data are loaded only once per rule definition + pub fn reset_rule_definition_registers(&mut self) { + self.current_input_register = None; + self.current_data_register = None; + } + + /// Push a new compilation context onto the context stack + pub fn push_context(&mut self, context: CompilationContext) { + self.context_stack.push(context); + } + + /// Pop the current compilation context from the context stack + pub fn pop_context(&mut self) -> Option { + // Don't pop the last context (default RegularRule) + if self.context_stack.len() > 1 { + self.context_stack.pop() + } else { + None + } + } + + /// Get the current scope mutably + fn current_scope_mut(&mut self) -> &mut Scope { + self.scopes.last_mut().expect("No active scope") + } + + /// Add a variable to the current scope (like interpreter's add_variable) + pub fn add_variable(&mut self, var_name: &str, register: Register) { + if var_name != "_" { + // Don't store anonymous variables + self.current_scope_mut() + .bound_vars + .insert(var_name.to_string(), register); + } + } + + /// Look up a variable in all scopes starting from innermost (like interpreter's lookup_local_var) + pub fn lookup_local_var(&self, var_name: &str) -> Option { + self.scopes + .iter() + .rev() + .find_map(|scope| scope.bound_vars.get(var_name).copied()) + } + + pub fn add_unbound_variable(&mut self, var_name: &str) { + self.current_scope_mut() + .unbound_vars + .insert(var_name.to_string()); + } + + pub fn is_unbound_var(&self, var_name: &str) -> bool { + self.lookup_local_var(var_name).is_none() + && self + .scopes + .iter() + .rev() + .any(|scope| scope.unbound_vars.contains(var_name)) + } + + pub fn bind_unbound_variable(&mut self, var_name: &str) { + self.current_scope_mut().unbound_vars.remove(var_name); + } + + pub(super) fn store_variable(&mut self, var_name: String, register: Register) { + self.add_variable(&var_name, register); + } + + /// Look up a variable register (backward compatibility) + pub(super) fn lookup_variable(&self, var_name: &str) -> Option { + self.lookup_local_var(var_name) + } + + pub(super) fn get_binding_plan_for_expr(&self, expr: &ExprRef) -> Option { + let module_idx = self.current_module_index; + let expr_idx = expr.as_ref().eidx(); + self.policy + .inner + .loop_hoisting_table + .get_expr_binding_plan(module_idx, expr_idx) + .cloned() + } + + pub(super) fn expect_binding_plan_for_expr( + &self, + expr: &ExprRef, + context: &str, + ) -> Result { + self.get_binding_plan_for_expr(expr) + .ok_or_else(|| CompilerError::MissingBindingPlan { + context: context.to_string(), + }) + } + + pub(super) fn resolve_variable(&mut self, var_name: &str, span: &Span) -> Result { + match var_name { + "input" => { + if let Some(register) = self.current_input_register { + return Ok(register); + } + + let dest = self.alloc_register(); + self.emit_instruction(Instruction::LoadInput { dest }, span); + self.current_input_register = Some(dest); + return Ok(dest); + } + "data" => { + if let Some(register) = self.current_data_register { + return Ok(register); + } + + let dest = self.alloc_register(); + self.emit_instruction(Instruction::LoadData { dest }, span); + self.current_data_register = Some(dest); + return Ok(dest); + } + _ => {} + } + + if let Some(var_reg) = self.lookup_variable(var_name) { + return Ok(var_reg); + } + + let rule_path = format!("{}.{}", &self.current_package, var_name); + let rule_index = self.get_or_assign_rule_index(&rule_path)?; + let dest = self.alloc_register(); + + self.emit_instruction(Instruction::CallRule { dest, rule_index }, span); + Ok(dest) + } + + pub fn emit_instruction(&mut self, instruction: Instruction, span: &Span) { + self.program.instructions.push(instruction); + + let source_path = span.source.get_path().to_string(); + let source_index = self.get_or_create_source_index(&source_path); + + self.spans + .push(SpanInfo::from_lexer_span(span, source_index)); + } + + fn get_or_create_source_index(&mut self, source_path: &str) -> usize { + if let Some(&index) = self.source_to_index.get(source_path) { + index + } else { + let index = self.source_to_index.len(); + self.source_to_index.insert(source_path.to_string(), index); + index + } + } +} diff --git a/src/languages/rego/compiler/destructuring.rs b/src/languages/rego/compiler/destructuring.rs new file mode 100644 index 0000000..edbc0dd --- /dev/null +++ b/src/languages/rego/compiler/destructuring.rs @@ -0,0 +1,363 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +use super::Compiler; +use super::Register; +use crate::compiler::destructuring_planner::plans::{ + AssignmentPlan, BindingPlan, DestructuringPlan, WildcardSide, +}; +use crate::lexer::Span; +use crate::rvm::instructions::Instruction; +use crate::value::Value; +use anyhow::{bail, Result}; + +#[derive(Clone, Copy, Debug)] +pub enum PlanContext { + ColonAssignment, + Assignment, + FunctionParameter, + LoopIndex, + SomeIn, +} + +impl PlanContext { + fn require_defined_values(self) -> bool { + matches!( + self, + PlanContext::Assignment + | PlanContext::FunctionParameter + | PlanContext::LoopIndex + | PlanContext::SomeIn + ) + } +} + +impl<'a> Compiler<'a> { + pub fn compile_assignment_plan_using_hoisted_destructuring( + &mut self, + plan: &AssignmentPlan, + span: &Span, + ) -> Result { + match plan { + AssignmentPlan::ColonEquals { + rhs_expr, lhs_plan, .. + } => { + let rhs_reg = self.compile_rego_expr_with_span(rhs_expr, rhs_expr.span(), false)?; + self.apply_destructuring_plan( + lhs_plan, + rhs_reg, + span, + PlanContext::ColonAssignment, + )?; + Ok(rhs_reg) + } + AssignmentPlan::EqualsBindLeft { + rhs_expr, lhs_plan, .. + } => { + let rhs_reg = self.compile_rego_expr_with_span(rhs_expr, rhs_expr.span(), false)?; + self.apply_destructuring_plan(lhs_plan, rhs_reg, span, PlanContext::Assignment)?; + Ok(self.load_bool_literal(true, span)) + } + AssignmentPlan::EqualsBindRight { + lhs_expr, rhs_plan, .. + } => { + let lhs_reg = self.compile_rego_expr_with_span(lhs_expr, lhs_expr.span(), false)?; + self.apply_destructuring_plan(rhs_plan, lhs_reg, span, PlanContext::Assignment)?; + Ok(self.load_bool_literal(true, span)) + } + AssignmentPlan::EqualsBothSides { element_pairs, .. } => { + for (value_expr, value_plan) in element_pairs { + let value_reg = + self.compile_rego_expr_with_span(value_expr, value_expr.span(), false)?; + self.apply_destructuring_plan( + value_plan, + value_reg, + span, + PlanContext::Assignment, + )?; + } + Ok(self.load_bool_literal(true, span)) + } + AssignmentPlan::EqualityCheck { lhs_expr, rhs_expr } => { + let lhs_reg = self.compile_rego_expr_with_span(lhs_expr, lhs_expr.span(), false)?; + let rhs_reg = self.compile_rego_expr_with_span(rhs_expr, rhs_expr.span(), false)?; + let dest = self.alloc_register(); + self.emit_instruction( + Instruction::Eq { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ); + Ok(dest) + } + AssignmentPlan::WildcardMatch { + lhs_expr, + rhs_expr, + wildcard_side, + } => match wildcard_side { + WildcardSide::Both => Ok(self.load_bool_literal(true, span)), + WildcardSide::Lhs => { + let rhs_reg = + self.compile_rego_expr_with_span(rhs_expr, rhs_expr.span(), false)?; + self.emit_instruction( + Instruction::AssertNotUndefined { register: rhs_reg }, + span, + ); + Ok(self.load_bool_literal(true, span)) + } + WildcardSide::Rhs => { + let lhs_reg = + self.compile_rego_expr_with_span(lhs_expr, lhs_expr.span(), false)?; + self.emit_instruction( + Instruction::AssertNotUndefined { register: lhs_reg }, + span, + ); + Ok(self.load_bool_literal(true, span)) + } + }, + } + } + + pub fn apply_binding_plan( + &mut self, + plan: &BindingPlan, + value_register: Register, + span: &Span, + ) -> Result<()> { + match plan { + BindingPlan::Assignment { .. } => { + bail!("assignment binding plans should be handled via compile_assignment_plan") + } + BindingPlan::LoopIndex { + destructuring_plan, .. + } => self.apply_destructuring_plan( + destructuring_plan, + value_register, + span, + PlanContext::LoopIndex, + ), + BindingPlan::Parameter { + destructuring_plan, .. + } => self.apply_destructuring_plan( + destructuring_plan, + value_register, + span, + PlanContext::FunctionParameter, + ), + BindingPlan::SomeIn { .. } => { + bail!("use apply_some_in_binding_plan for SomeIn bindings") + } + } + } + + pub fn apply_some_in_binding_plan( + &mut self, + key_plan: Option<&DestructuringPlan>, + key_register: Option, + value_plan: &DestructuringPlan, + value_register: Register, + span: &Span, + ) -> Result<()> { + if let (Some(plan), Some(register)) = (key_plan, key_register) { + self.apply_destructuring_plan(plan, register, span, PlanContext::SomeIn)?; + } + self.apply_destructuring_plan(value_plan, value_register, span, PlanContext::SomeIn) + } + + fn apply_destructuring_plan( + &mut self, + plan: &DestructuringPlan, + value_register: Register, + span: &Span, + context: PlanContext, + ) -> Result<()> { + match plan { + DestructuringPlan::Var(name_span) => { + self.bind_variable(name_span, value_register, span, context)?; + } + DestructuringPlan::Ignore => {} + DestructuringPlan::EqualityExpr(expected_expr) => { + let expected_reg = + self.compile_rego_expr_with_span(expected_expr, expected_expr.span(), false)?; + let cmp_reg = self.alloc_register(); + self.emit_instruction( + Instruction::Eq { + dest: cmp_reg, + left: value_register, + right: expected_reg, + }, + span, + ); + self.emit_instruction(Instruction::AssertCondition { condition: cmp_reg }, span); + } + DestructuringPlan::EqualityValue(expected_value) => { + let expected_reg = self.load_literal_value(expected_value, span); + let cmp_reg = self.alloc_register(); + self.emit_instruction( + Instruction::Eq { + dest: cmp_reg, + left: value_register, + right: expected_reg, + }, + span, + ); + self.emit_instruction(Instruction::AssertCondition { condition: cmp_reg }, span); + } + DestructuringPlan::Array { element_plans } => { + self.assert_array_length(value_register, element_plans.len(), span)?; + for (index, element_plan) in element_plans.iter().enumerate() { + let literal_idx = self.add_literal(Value::from(index)); + let element_reg = self.alloc_register(); + self.emit_instruction( + Instruction::IndexLiteral { + dest: element_reg, + container: value_register, + literal_idx, + }, + span, + ); + if context.require_defined_values() { + self.emit_instruction( + Instruction::AssertNotUndefined { + register: element_reg, + }, + span, + ); + } + self.apply_destructuring_plan(element_plan, element_reg, span, context)?; + } + } + DestructuringPlan::Object { + field_plans, + dynamic_fields, + } => { + for (key, field_plan) in field_plans { + let literal_idx = self.add_literal(key.clone()); + let field_reg = self.alloc_register(); + self.emit_instruction( + Instruction::IndexLiteral { + dest: field_reg, + container: value_register, + literal_idx, + }, + span, + ); + self.emit_instruction( + Instruction::AssertNotUndefined { + register: field_reg, + }, + span, + ); + self.apply_destructuring_plan(field_plan, field_reg, span, context)?; + } + + for (key_expr, field_plan) in dynamic_fields { + let key_reg = + self.compile_rego_expr_with_span(key_expr, key_expr.span(), false)?; + let field_reg = self.alloc_register(); + self.emit_instruction( + Instruction::Index { + dest: field_reg, + container: value_register, + key: key_reg, + }, + span, + ); + self.emit_instruction( + Instruction::AssertNotUndefined { + register: field_reg, + }, + span, + ); + self.apply_destructuring_plan(field_plan, field_reg, span, context)?; + } + } + } + Ok(()) + } + + fn bind_variable( + &mut self, + name_span: &Span, + value_register: Register, + span: &Span, + context: PlanContext, + ) -> Result<()> { + let var_name = name_span.text(); + if var_name == "_" { + return Ok(()); + } + + if self.lookup_local_var(var_name).is_some() { + bail!("Variable '{var_name}' already defined in current scope"); + } + + let dest = self.alloc_register(); + self.emit_instruction( + Instruction::Move { + dest, + src: value_register, + }, + span, + ); + self.add_variable(var_name, dest); + + if context.require_defined_values() { + self.emit_instruction(Instruction::AssertNotUndefined { register: dest }, span); + } + + Ok(()) + } + + fn load_bool_literal(&mut self, value: bool, span: &Span) -> Register { + let dest = self.alloc_register(); + self.emit_instruction(Instruction::LoadBool { dest, value }, span); + dest + } + + fn load_literal_value(&mut self, value: &Value, span: &Span) -> Register { + let literal_idx = self.add_literal(value.clone()); + let dest = self.alloc_register(); + self.emit_instruction(Instruction::Load { dest, literal_idx }, span); + dest + } + + fn assert_array_length( + &mut self, + array_register: Register, + expected_length: usize, + span: &Span, + ) -> Result<()> { + let expected_literal = self.add_literal(Value::from(expected_length)); + let actual_len_reg = self.alloc_register(); + self.emit_instruction( + Instruction::Count { + dest: actual_len_reg, + collection: array_register, + }, + span, + ); + + let expected_len_reg = self.alloc_register(); + self.emit_instruction( + Instruction::Load { + dest: expected_len_reg, + literal_idx: expected_literal, + }, + span, + ); + + let cmp_reg = self.alloc_register(); + self.emit_instruction( + Instruction::Eq { + dest: cmp_reg, + left: actual_len_reg, + right: expected_len_reg, + }, + span, + ); + self.emit_instruction(Instruction::AssertCondition { condition: cmp_reg }, span); + Ok(()) + } +} diff --git a/src/languages/rego/compiler/error.rs b/src/languages/rego/compiler/error.rs new file mode 100644 index 0000000..1a457f0 --- /dev/null +++ b/src/languages/rego/compiler/error.rs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use alloc::format; +use alloc::string::String; + +#[derive(thiserror::Error, Debug)] +pub enum CompilerError { + #[error("Not a builtin function: {name}")] + NotBuiltinFunction { name: String }, + + #[error("Unknown builtin function: {name}")] + UnknownBuiltinFunction { name: String }, + + #[error("internal: missing context for yield")] + MissingYieldContext, + + #[error( + "Direct access to 'data' root is not allowed. Use a specific path like 'data.package.rule'" + )] + DirectDataAccess, + + #[error("Not a simple reference chain")] + NotSimpleReferenceChain, + + #[error("Missing binding plan for {context}")] + MissingBindingPlan { context: String }, + + #[error("Unexpected binding plan variant for {context}: {found}")] + UnexpectedBindingPlan { context: String, found: String }, + + #[error("Invalid destructuring pattern in assignment")] + InvalidDestructuringPattern, + + #[error("Unsupported expression type in chained reference")] + UnsupportedChainedExpression, + + #[error("internal: no rule type found for '{rule_path}'")] + RuleTypeNotFound { rule_path: String }, + + #[error("unary - can only be used with numeric literals")] + InvalidUnaryMinus, + + #[error("Unknown function: '{name}'")] + UnknownFunction { name: String }, + + #[error("Undefined variable: '{name}'")] + UndefinedVariable { name: String }, + + #[error("SomeIn should have been hoisted as a loop")] + SomeInNotHoisted, + + #[error("Invalid function expression")] + InvalidFunctionExpression, + + #[error("Invalid function expression with package")] + InvalidFunctionExpressionWithPackage, + + #[error("Compilation error: {message}")] + General { message: String }, +} + +impl From for CompilerError { + fn from(err: anyhow::Error) -> Self { + CompilerError::General { + message: format!("{}", err), + } + } +} + +pub type Result = ::core::result::Result; diff --git a/src/languages/rego/compiler/expressions.rs b/src/languages/rego/compiler/expressions.rs new file mode 100644 index 0000000..5395968 --- /dev/null +++ b/src/languages/rego/compiler/expressions.rs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod collection_literals; +mod operations; + +use super::{Compiler, CompilerError, Register, Result}; +use crate::ast::{Expr, ExprRef}; +use crate::compiler::destructuring_planner::plans::BindingPlan; +use crate::lexer::Span; +use crate::rvm::Instruction; +use crate::Value; +use alloc::{format, string::ToString}; + +impl<'a> Compiler<'a> { + /// Compile a Rego expression to RVM instructions + pub fn compile_rego_expr(&mut self, expr: &ExprRef) -> Result { + self.compile_rego_expr_with_span(expr, expr.span(), false) + } + + /// Compile a Rego expression to RVM instructions with span tracking + pub fn compile_rego_expr_with_span( + &mut self, + expr: &ExprRef, + span: &Span, + assert_condition: bool, + ) -> Result { + if let Some(reg) = self.loop_expr_register_map.get(expr).cloned() { + let result_reg = reg; + if assert_condition { + self.emit_instruction( + Instruction::AssertCondition { + condition: result_reg, + }, + span, + ); + } + return Ok(result_reg); + } + + let result_reg = match expr.as_ref() { + Expr::Number { value, .. } + | Expr::String { value, .. } + | Expr::RawString { value, .. } + | Expr::Bool { value, .. } => { + let dest = self.alloc_register(); + let literal_idx = self.add_literal(value.clone()); + self.emit_instruction(Instruction::Load { dest, literal_idx }, span); + dest + } + Expr::Null { .. } => { + let dest = self.alloc_register(); + let literal_idx = self.add_literal(Value::Null); + self.emit_instruction(Instruction::Load { dest, literal_idx }, span); + dest + } + Expr::Array { items, .. } => self.compile_array_literal(items, span)?, + Expr::Set { items, .. } => self.compile_set_literal(items, span)?, + Expr::Object { fields, .. } => self.compile_object_literal(fields, span)?, + Expr::ArithExpr { lhs, op, rhs, .. } => self.compile_arith_expr(lhs, rhs, op, span)?, + Expr::BoolExpr { lhs, op, rhs, .. } => self.compile_bool_expr(lhs, rhs, op, span)?, + Expr::AssignExpr { .. } => { + let binding_plan = + self.expect_binding_plan_for_expr(expr, "assignment expression")?; + + let result: Result = match binding_plan { + BindingPlan::Assignment { plan } => self + .compile_assignment_plan_using_hoisted_destructuring(&plan, span) + .map_err(CompilerError::from), + other => Err(CompilerError::UnexpectedBindingPlan { + context: "assignment expression".to_string(), + found: format!("{other:?}"), + }), + }; + + return result; + } + Expr::Var { value, .. } => { + if let Value::String(_var_name) = value { + self.compile_chained_ref(expr, span)? + } else { + let dest = self.alloc_register(); + let literal_idx = self.add_literal(value.clone()); + self.emit_instruction(Instruction::Load { dest, literal_idx }, span); + dest + } + } + Expr::RefDot { .. } | Expr::RefBrack { .. } => self.compile_chained_ref(expr, span)?, + Expr::Membership { + value, collection, .. + } => self.compile_membership(value, collection, span)?, + Expr::ArrayCompr { term, query, .. } => { + self.compile_array_comprehension(term, query, span)? + } + Expr::SetCompr { term, query, .. } => { + self.compile_set_comprehension(term, query, span)? + } + Expr::ObjectCompr { + key, value, query, .. + } => self.compile_object_comprehension(key, value, query, span)?, + Expr::Call { fcn, params, .. } => { + self.compile_function_call(fcn, params, span.clone())? + } + Expr::UnaryExpr { expr, .. } => self.compile_unary_minus(expr, span)?, + Expr::BinExpr { op, lhs, rhs, .. } => self.compile_bin_expr(lhs, rhs, op, span)?, + #[cfg(feature = "rego-extensions")] + Expr::OrExpr { lhs, rhs, .. } => self.compile_or_expr(lhs, rhs, span)?, + }; + + if assert_condition { + self.emit_instruction( + Instruction::AssertCondition { + condition: result_reg, + }, + span, + ); + } + + Ok(result_reg) + } +} diff --git a/src/languages/rego/compiler/expressions/collection_literals.rs b/src/languages/rego/compiler/expressions/collection_literals.rs new file mode 100644 index 0000000..fc34dfa --- /dev/null +++ b/src/languages/rego/compiler/expressions/collection_literals.rs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{Compiler, Register, Result}; +use crate::ast::ExprRef; +use crate::lexer::Span; +use crate::rvm::instructions::{ArrayCreateParams, ObjectCreateParams, SetCreateParams}; +use crate::rvm::Instruction; +use crate::{Rc, Value}; +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +impl<'a> Compiler<'a> { + pub(super) fn compile_array_literal( + &mut self, + items: &[ExprRef], + span: &Span, + ) -> Result { + let mut element_registers = Vec::with_capacity(items.len()); + for item in items { + let item_reg = self.compile_rego_expr_with_span(item, item.span(), false)?; + element_registers.push(item_reg); + } + + let dest = self.alloc_register(); + let params = ArrayCreateParams { + dest, + elements: element_registers, + }; + let params_index = self + .program + .instruction_data + .add_array_create_params(params); + self.emit_instruction(Instruction::ArrayCreate { params_index }, span); + Ok(dest) + } + + pub(super) fn compile_set_literal( + &mut self, + items: &[ExprRef], + span: &Span, + ) -> Result { + let mut element_registers = Vec::with_capacity(items.len()); + for item in items { + let item_reg = self.compile_rego_expr_with_span(item, item.span(), false)?; + element_registers.push(item_reg); + } + + let dest = self.alloc_register(); + let params = SetCreateParams { + dest, + elements: element_registers, + }; + let params_index = self.program.instruction_data.add_set_create_params(params); + self.emit_instruction(Instruction::SetCreate { params_index }, span); + Ok(dest) + } + + pub(super) fn compile_object_literal( + &mut self, + fields: &[(crate::lexer::Span, ExprRef, ExprRef)], + span: &Span, + ) -> Result { + let dest = self.alloc_register(); + + let mut value_regs = Vec::with_capacity(fields.len()); + for (_, _key_expr, value_expr) in fields { + let value_reg = + self.compile_rego_expr_with_span(value_expr, value_expr.span(), false)?; + value_regs.push(value_reg); + } + + let mut literal_key_fields = Vec::new(); + let mut non_literal_key_fields = Vec::new(); + let mut literal_keys: Vec = Vec::new(); + + for (field_idx, (_, key_expr, _value_expr)) in fields.iter().enumerate() { + let value_reg = value_regs[field_idx]; + let key_literal = match key_expr.as_ref() { + crate::ast::Expr::String { value, .. } + | crate::ast::Expr::RawString { value, .. } + | crate::ast::Expr::Number { value, .. } + | crate::ast::Expr::Bool { value, .. } + | crate::ast::Expr::Null { value, .. } => Some(value.clone()), + _ => None, + }; + + if let Some(key_value) = key_literal { + let literal_idx = self.add_literal(key_value.clone()); + literal_key_fields.push((literal_idx, value_reg)); + literal_keys.push(key_value); + } else { + let key_reg = self.compile_rego_expr_with_span(key_expr, key_expr.span(), false)?; + non_literal_key_fields.push((key_reg, value_reg)); + } + } + + let template_literal_idx = { + let mut template_keys = literal_keys.clone(); + template_keys.sort(); + + let mut template_obj = BTreeMap::new(); + for key in &template_keys { + template_obj.insert(key.clone(), Value::Undefined); + } + + let template_value = Value::Object(Rc::new(template_obj)); + self.add_literal(template_value) + }; + + literal_key_fields.sort_by(|a, b| { + let key_a = &self.program.literals[a.0 as usize]; + let key_b = &self.program.literals[b.0 as usize]; + key_a.cmp(key_b) + }); + + let params = ObjectCreateParams { + dest, + template_literal_idx, + literal_key_fields, + fields: non_literal_key_fields, + }; + let params_index = self + .program + .instruction_data + .add_object_create_params(params); + self.emit_instruction(Instruction::ObjectCreate { params_index }, span); + Ok(dest) + } +} diff --git a/src/languages/rego/compiler/expressions/operations.rs b/src/languages/rego/compiler/expressions/operations.rs new file mode 100644 index 0000000..a24887b --- /dev/null +++ b/src/languages/rego/compiler/expressions/operations.rs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{Compiler, CompilerError, Register, Result}; +use crate::ast::{ArithOp, BinOp, BoolOp, ExprRef}; +use crate::lexer::Span; +use crate::rvm::instructions::BuiltinCallParams; +use crate::rvm::Instruction; +use crate::Value; + +impl<'a> Compiler<'a> { + pub(super) fn compile_arith_expr( + &mut self, + lhs: &ExprRef, + rhs: &ExprRef, + op: &ArithOp, + span: &Span, + ) -> Result { + let lhs_reg = self.compile_rego_expr_with_span(lhs, lhs.span(), false)?; + let rhs_reg = self.compile_rego_expr_with_span(rhs, rhs.span(), false)?; + let dest = self.alloc_register(); + + match op { + ArithOp::Add => self.emit_instruction( + Instruction::Add { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ), + ArithOp::Sub => self.emit_instruction( + Instruction::Sub { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ), + ArithOp::Mul => self.emit_instruction( + Instruction::Mul { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ), + ArithOp::Div => self.emit_instruction( + Instruction::Div { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ), + ArithOp::Mod => self.emit_instruction( + Instruction::Mod { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ), + } + Ok(dest) + } + + pub(super) fn compile_bool_expr( + &mut self, + lhs: &ExprRef, + rhs: &ExprRef, + op: &BoolOp, + span: &Span, + ) -> Result { + let lhs_reg = self.compile_rego_expr_with_span(lhs, lhs.span(), false)?; + let rhs_reg = self.compile_rego_expr_with_span(rhs, rhs.span(), false)?; + let dest = self.alloc_register(); + + match op { + BoolOp::Eq => self.emit_instruction( + Instruction::Eq { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ), + BoolOp::Lt => self.emit_instruction( + Instruction::Lt { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ), + BoolOp::Gt => self.emit_instruction( + Instruction::Gt { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ), + BoolOp::Ge => self.emit_instruction( + Instruction::Ge { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ), + BoolOp::Le => self.emit_instruction( + Instruction::Le { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ), + BoolOp::Ne => self.emit_instruction( + Instruction::Ne { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ), + } + Ok(dest) + } + + pub(super) fn compile_bin_expr( + &mut self, + lhs: &ExprRef, + rhs: &ExprRef, + op: &BinOp, + span: &Span, + ) -> Result { + let lhs_reg = self.compile_rego_expr_with_span(lhs, lhs.span(), false)?; + let rhs_reg = self.compile_rego_expr_with_span(rhs, rhs.span(), false)?; + let dest = self.alloc_register(); + + match op { + BinOp::Union => { + let builtin_index = self.get_builtin_index("sets.union")?; + let params = BuiltinCallParams { + dest, + builtin_index, + num_args: 2, + args: [lhs_reg, rhs_reg, 0, 0, 0, 0, 0, 0], + }; + let params_index = self + .program + .instruction_data + .add_builtin_call_params(params); + self.emit_instruction(Instruction::BuiltinCall { params_index }, span); + } + BinOp::Intersection => { + let builtin_index = self.get_builtin_index("sets.intersection")?; + let params = BuiltinCallParams { + dest, + builtin_index, + num_args: 2, + args: [lhs_reg, rhs_reg, 0, 0, 0, 0, 0, 0], + }; + let params_index = self + .program + .instruction_data + .add_builtin_call_params(params); + self.emit_instruction(Instruction::BuiltinCall { params_index }, span); + } + } + Ok(dest) + } + + pub(super) fn compile_membership( + &mut self, + value: &ExprRef, + collection: &ExprRef, + span: &Span, + ) -> Result { + let value_reg = self.compile_rego_expr_with_span(value, value.span(), false)?; + let collection_reg = + self.compile_rego_expr_with_span(collection, collection.span(), false)?; + + let dest = self.alloc_register(); + self.emit_instruction( + Instruction::Contains { + dest, + collection: collection_reg, + value: value_reg, + }, + span, + ); + Ok(dest) + } + + pub(super) fn compile_unary_minus(&mut self, expr: &ExprRef, span: &Span) -> Result { + match expr.as_ref() { + crate::ast::Expr::Number { .. } if !expr.span().text().starts_with('-') => { + let operand_reg = self.compile_rego_expr_with_span(expr, expr.span(), false)?; + let zero_literal_idx = self.add_literal(Value::from(0)); + let zero_reg = self.alloc_register(); + self.emit_instruction( + Instruction::Load { + dest: zero_reg, + literal_idx: zero_literal_idx, + }, + span, + ); + + let dest = self.alloc_register(); + self.emit_instruction( + Instruction::Sub { + dest, + left: zero_reg, + right: operand_reg, + }, + span, + ); + Ok(dest) + } + _ => Err(CompilerError::InvalidUnaryMinus), + } + } + + #[cfg(feature = "rego-extensions")] + pub(super) fn compile_or_expr( + &mut self, + lhs: &ExprRef, + rhs: &ExprRef, + span: &Span, + ) -> Result { + let lhs_reg = self.compile_rego_expr_with_span(lhs, lhs.span(), false)?; + let rhs_reg = self.compile_rego_expr_with_span(rhs, rhs.span(), false)?; + + let dest = self.alloc_register(); + self.emit_instruction( + Instruction::Or { + dest, + left: lhs_reg, + right: rhs_reg, + }, + span, + ); + Ok(dest) + } +} diff --git a/src/languages/rego/compiler/function_calls.rs b/src/languages/rego/compiler/function_calls.rs new file mode 100644 index 0000000..c98418a --- /dev/null +++ b/src/languages/rego/compiler/function_calls.rs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{Compiler, CompilerError, Register, Result}; +use crate::ast::ExprRef; +use crate::lexer::Span; +use crate::rvm::instructions::{BuiltinCallParams, FunctionCallParams}; +use crate::rvm::Instruction; +use crate::utils::get_path_string; +use alloc::vec::Vec; + +impl<'a> Compiler<'a> { + pub(super) fn compile_function_call( + &mut self, + fcn: &ExprRef, + params: &[ExprRef], + span: Span, + ) -> Result { + let fcn_path = + get_path_string(fcn, None).map_err(|_| CompilerError::InvalidFunctionExpression)?; + + let original_fcn_path = fcn_path.clone(); + let full_fcn_path = if self.policy.inner.rules.contains_key(&fcn_path) { + fcn_path + } else { + get_path_string(fcn, Some(&self.current_package)) + .map_err(|_| CompilerError::InvalidFunctionExpressionWithPackage)? + }; + + let mut arg_regs = Vec::new(); + for param in params.iter() { + let param_reg = self.compile_rego_expr_with_span(param, param.span(), false)?; + arg_regs.push(param_reg); + } + + let dest = self.alloc_register(); + + if self.is_user_defined_function(&full_fcn_path) { + let rule_index = self.get_or_assign_rule_index(&full_fcn_path)?; + let mut args_array = [0u8; 8]; + let num_args = arg_regs.len().min(8) as u8; + for (i, ®) in arg_regs.iter().take(8).enumerate() { + args_array[i] = reg; + } + + let params_index = self.program.add_function_call_params(FunctionCallParams { + func_rule_index: rule_index, + dest, + num_args, + args: args_array, + }); + self.emit_instruction(Instruction::FunctionCall { params_index }, &span); + } else if self.is_builtin(&original_fcn_path) { + let builtin_index = self.get_builtin_index(&original_fcn_path)?; + let mut args_array = [0u8; 8]; + let num_args = arg_regs.len().min(8) as u8; + for (i, ®) in arg_regs.iter().take(8).enumerate() { + args_array[i] = reg; + } + + let params_index = self.program.add_builtin_call_params(BuiltinCallParams { + dest, + builtin_index, + num_args, + args: args_array, + }); + self.emit_instruction(Instruction::BuiltinCall { params_index }, &span); + } else { + return Err(CompilerError::UnknownFunction { + name: original_fcn_path, + }); + } + + Ok(dest) + } +} diff --git a/src/languages/rego/compiler/loops.rs b/src/languages/rego/compiler/loops.rs new file mode 100644 index 0000000..1d4f5ac --- /dev/null +++ b/src/languages/rego/compiler/loops.rs @@ -0,0 +1,406 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{CompilationContext, Compiler, CompilerError, ContextType, Register, Result}; +use crate::ast::{self, ExprRef, LiteralStmt, Query}; +use crate::compiler::destructuring_planner::plans::BindingPlan; +use crate::compiler::hoist::{HoistedLoop, LoopType}; +use crate::lexer::Span; +use crate::rvm::instructions::{LoopMode, LoopStartParams}; +use crate::rvm::Instruction; +use crate::Value; +use alloc::format; +use alloc::string::ToString; +use alloc::vec::Vec; + +impl<'a> Compiler<'a> { + pub(super) fn get_statement_loops(&self, stmt: &LiteralStmt) -> Result> { + self.policy + .inner + .loop_hoisting_table + .get_statement_loops(self.current_module_index, stmt.sidx) + .cloned() + .ok_or_else(|| CompilerError::General { + message: format!( + "missing loop hoisting data for statement at {}:{}", + stmt.span.line, stmt.span.col + ), + }) + } + + pub(super) fn get_expr_loops(&self, expr: &ExprRef) -> Vec { + let module_idx = self.current_module_index; + let expr_idx = expr.as_ref().eidx(); + self.policy + .inner + .loop_hoisting_table + .get_expr_loops(module_idx, expr_idx) + .cloned() + .unwrap_or_default() + } + + pub(super) fn compile_hoisted_loops( + &mut self, + stmts: &[&LiteralStmt], + loops: &[HoistedLoop], + ) -> Result<()> { + if loops.is_empty() { + if !stmts.is_empty() { + self.compile_single_statement(stmts[0])?; + return self.hoist_loops_and_compile_statements(&stmts[1..]); + } else { + self.hoist_loops_and_emit_context_yield()?; + } + } + + let current_loop = &loops[0]; + let remaining_loops = &loops[1..]; + + match current_loop.loop_type { + LoopType::IndexIteration => { + self.compile_index_iteration_loop( + ¤t_loop.loop_expr, + ¤t_loop.key, + ¤t_loop.value, + ¤t_loop.collection, + stmts, + remaining_loops, + )?; + Ok(()) + } + LoopType::Walk => Err(CompilerError::General { + message: "walk loops are not yet supported in the RVM compiler".to_string(), + }), + } + } + + pub(super) fn compile_every_quantifier( + &mut self, + key: &Option, + value: &Span, + domain: &ExprRef, + query: &Query, + span: &Span, + ) -> Result<()> { + let collection_reg = self.compile_rego_expr(domain)?; + + let key_reg = self.alloc_register(); + let value_reg = self.alloc_register(); + let result_reg = self.alloc_register(); + + let value_var_name = value.text().to_string(); + let key_var_name = key.as_ref().map(|k| k.text().to_string()); + + let actual_key_reg = if key_var_name.is_none() || key_var_name.as_deref() == Some("_") { + value_reg + } else { + key_reg + }; + + let loop_params_index = self.program.add_loop_params(LoopStartParams { + mode: LoopMode::Every, + collection: collection_reg, + key_reg: actual_key_reg, + value_reg, + result_reg, + body_start: 0, + loop_end: 0, + }); + + self.emit_instruction( + Instruction::LoopStart { + params_index: loop_params_index, + }, + span, + ); + + let body_start = self.program.instructions.len() as u16; + + self.push_scope(); + + let every_context = CompilationContext { + context_type: ContextType::Every, + dest_register: result_reg, + key_expr: None, + value_expr: None, + span: span.clone(), + key_value_loops_hoisted: false, + }; + self.push_context(every_context); + + self.add_variable(&value_var_name, value_reg); + if let Some(ref key_name) = key_var_name { + self.add_variable(key_name, key_reg); + } + + self.compile_query(query)?; + + self.pop_context(); + self.pop_scope(); + + self.emit_instruction( + Instruction::LoopNext { + body_start, + loop_end: 0, + }, + span, + ); + + let loop_end = self.program.instructions.len() as u16; + + self.program + .update_loop_params(loop_params_index, |params| { + params.body_start = body_start; + params.loop_end = loop_end; + }); + + let loop_next_idx = self.program.instructions.len() - 1; + if let Instruction::LoopNext { + loop_end: ref mut end, + .. + } = &mut self.program.instructions[loop_next_idx] + { + *end = loop_end; + } + + Ok(()) + } + + fn compile_index_iteration_loop( + &mut self, + loop_expr: &Option, + key_var: &Option, + _value_var: &ExprRef, + collection: &ExprRef, + remaining_stmts: &[&LiteralStmt], + remaining_loops: &[HoistedLoop], + ) -> Result<()> { + let collection_reg = self.compile_rego_expr(collection)?; + + let key_reg = self.alloc_register(); + let value_reg = self.alloc_register(); + let result_reg = self.alloc_register(); + + if let Some(loop_expr) = loop_expr { + self.loop_expr_register_map + .insert(loop_expr.clone(), value_reg); + } + + let mut key_binding_plan: Option<(BindingPlan, Span)> = None; + if let Some(key_var) = key_var { + if let Some(binding_plan) = self.get_binding_plan_for_expr(key_var) { + if let BindingPlan::LoopIndex { .. } = &binding_plan { + key_binding_plan = Some((binding_plan, key_var.span().clone())); + } else { + return Err(CompilerError::UnexpectedBindingPlan { + context: format!("loop index pattern {}", key_var.span().text()), + found: format!("{binding_plan:?}"), + }); + } + } else { + match key_var.as_ref() { + ast::Expr::Var { value, .. } => { + let var_name = match value { + Value::String(s) => { + if s.as_ref() == "_" { + "".to_string() + } else { + s.to_string() + } + } + _ => value.to_string(), + }; + if !var_name.is_empty() && var_name != "_" { + self.store_variable(var_name, key_reg); + } + } + _ => { + return Err(CompilerError::MissingBindingPlan { + context: format!("loop index pattern {}", key_var.span().text()), + }); + } + } + } + self.loop_expr_register_map.insert(key_var.clone(), key_reg); + } + + let loop_params_index = self.program.add_loop_params(LoopStartParams { + mode: LoopMode::ForEach, + collection: collection_reg, + key_reg, + value_reg, + result_reg, + body_start: 0, + loop_end: 0, + }); + self.emit_instruction( + Instruction::LoopStart { + params_index: loop_params_index, + }, + collection.span(), + ); + + let body_start = self.program.instructions.len() as u16; + + if let Some((binding_plan, plan_span)) = key_binding_plan.as_ref() { + self.apply_binding_plan(binding_plan, key_reg, plan_span) + .map_err(CompilerError::from)?; + } + + let body_stmts = &remaining_stmts[0..]; + self.compile_hoisted_loops(body_stmts, remaining_loops)?; + + self.emit_instruction( + Instruction::LoopNext { + body_start, + loop_end: 0, + }, + collection.span(), + ); + + let loop_end = self.program.instructions.len() as u16; + + self.program + .update_loop_params(loop_params_index, |params| { + params.body_start = body_start; + params.loop_end = loop_end; + }); + + let loop_next_idx = self.program.instructions.len() - 1; + if let Instruction::LoopNext { + loop_end: ref mut end, + .. + } = &mut self.program.instructions[loop_next_idx] + { + *end = loop_end; + } + + Ok(()) + } + + pub(super) fn compile_some_in_loop_with_remaining_statements( + &mut self, + key: &Option, + value: &ExprRef, + collection: &ExprRef, + remaining_stmts: &[&LiteralStmt], + ) -> Result { + let loop_body_stmts = &remaining_stmts[1..]; + self.compile_some_in_loop_with_body(key, value, collection, loop_body_stmts) + } + + fn compile_some_in_loop_with_body( + &mut self, + key: &Option, + value: &ExprRef, + collection: &ExprRef, + loop_body_stmts: &[&LiteralStmt], + ) -> Result { + let collection_reg = self.compile_rego_expr(collection)?; + + let key_reg = self.alloc_register(); + let value_reg = self.alloc_register(); + let result_reg = self.alloc_register(); + + let loop_params_index = self.program.add_loop_params(LoopStartParams { + mode: LoopMode::ForEach, + collection: collection_reg, + key_reg, + value_reg, + result_reg, + body_start: 0, + loop_end: 0, + }); + self.emit_instruction( + Instruction::LoopStart { + params_index: loop_params_index, + }, + collection.span(), + ); + + let body_start = self.program.instructions.len() as u16; + + if let Some(binding_plan) = self.get_binding_plan_for_expr(collection) { + if let BindingPlan::SomeIn { + key_plan, + value_plan, + .. + } = &binding_plan + { + let key_register = key_plan.as_ref().map(|_| key_reg); + self.apply_some_in_binding_plan( + key_plan.as_ref(), + key_register, + value_plan, + value_reg, + collection.span(), + ) + .map_err(CompilerError::from)?; + } else { + return Err(CompilerError::UnexpectedBindingPlan { + context: format!("some-in binding {}", collection.span().text()), + found: format!("{binding_plan:?}"), + }); + } + } else { + if let Some(key_expr) = key { + match key_expr.as_ref() { + ast::Expr::Var { + value: var_name, .. + } => { + let var_name = var_name.as_string()?.to_string(); + self.store_variable(var_name, key_reg); + } + _ => { + return Err(CompilerError::MissingBindingPlan { + context: format!("some-in key pattern {}", key_expr.span().text()), + }); + } + } + } + + match value.as_ref() { + ast::Expr::Var { + value: var_name, .. + } => { + let var_name = var_name.as_string()?.to_string(); + self.store_variable(var_name, value_reg); + } + _ => { + return Err(CompilerError::MissingBindingPlan { + context: format!("some-in value pattern {}", value.span().text()), + }); + } + } + } + + self.hoist_loops_and_compile_statements(loop_body_stmts)?; + + self.emit_instruction( + Instruction::LoopNext { + body_start, + loop_end: 0, + }, + collection.span(), + ); + + let loop_end = self.program.instructions.len() as u16; + + self.program + .update_loop_params(loop_params_index, |params| { + params.body_start = body_start; + params.loop_end = loop_end; + }); + + let loop_next_idx = self.program.instructions.len() - 1; + if let Instruction::LoopNext { + loop_end: ref mut end, + .. + } = &mut self.program.instructions[loop_next_idx] + { + *end = loop_end; + } + + Ok(result_reg) + } +} diff --git a/src/languages/rego/compiler/mod.rs b/src/languages/rego/compiler/mod.rs new file mode 100644 index 0000000..ac131fc --- /dev/null +++ b/src/languages/rego/compiler/mod.rs @@ -0,0 +1,162 @@ +mod comprehensions; +mod core; +mod destructuring; +mod error; +mod expressions; +mod function_calls; +mod loops; +mod program; +mod queries; +mod references; +mod rules; + +pub use error::{CompilerError, Result}; + +use crate::ast::ExprRef; +use crate::lexer::Span; +use crate::rvm::program::{Program, RuleType, SpanInfo}; +use crate::CompiledPolicy; +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use indexmap::IndexMap; + +pub type Register = u8; + +#[derive(Debug, Clone, Default)] +struct Scope { + bound_vars: BTreeMap, + unbound_vars: BTreeSet, +} + +#[derive(Debug, Clone)] +pub enum ComprehensionType { + Array, + Object, + Set, +} + +#[derive(Debug, Clone)] +pub enum ContextType { + Comprehension(ComprehensionType), + Rule(RuleType), + Every, +} + +/// Compilation context for handling different types of rule bodies and comprehensions +#[derive(Debug, Clone)] +pub struct CompilationContext { + pub(super) context_type: ContextType, + pub(super) dest_register: Register, + pub(super) key_expr: Option, + pub(super) value_expr: Option, + pub(super) span: Span, + pub(super) key_value_loops_hoisted: bool, +} + +/// Entry in the rule compilation worklist that tracks both rule path and full call stack for recursion detection +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct WorklistEntry { + /// Rule path to be compiled (e.g., "data.package.rule") + pub rule_path: String, + /// Call stack of rule indices leading to this rule (empty for entry point) + pub call_stack: Vec, +} + +impl WorklistEntry { + pub fn new(rule_path: String, call_stack: Vec) -> Self { + Self { + rule_path, + call_stack, + } + } + + pub fn entry_point(rule_path: String) -> Self { + Self { + rule_path, + call_stack: Vec::new(), + } + } + + /// Create a new entry by extending the call stack with the caller's rule index + pub fn with_caller( + rule_path: String, + current_call_stack: &[u16], + caller_rule_index: u16, + ) -> Self { + let mut new_call_stack = current_call_stack.to_vec(); + new_call_stack.push(caller_rule_index); + Self { + rule_path, + call_stack: new_call_stack, + } + } + + /// Check if this entry would create a recursive call + pub fn would_create_recursion(&self, target_rule_index: u16) -> bool { + self.call_stack.contains(&target_rule_index) + } +} + +pub struct Compiler<'a> { + program: Program, + spans: Vec, + register_counter: Register, + scopes: Vec, + policy: &'a CompiledPolicy, + current_package: String, + current_module_index: u32, + rule_index_map: BTreeMap, + rule_worklist: Vec, + rule_definitions: Vec>>, + rule_definition_function_params: Vec>>>, + rule_definition_destructuring_patterns: Vec>>, + rule_types: Vec, + rule_function_param_count: Vec>, + rule_result_registers: Vec, + rule_num_registers: Vec, + context_stack: Vec, + loop_expr_register_map: BTreeMap, + source_to_index: BTreeMap, + builtin_index_map: BTreeMap, + current_input_register: Option, + current_data_register: Option, + current_rule_path: String, + current_call_stack: Vec, + entry_points: IndexMap, +} + +impl<'a> Compiler<'a> { + pub fn with_policy(policy: &'a CompiledPolicy) -> Self { + let mut program = Program::new(); + program.rego_v0 = policy.is_rego_v0(); + Self { + program, + spans: Vec::new(), + register_counter: 1, + scopes: vec![Scope::default()], + policy, + current_package: String::new(), + current_module_index: 0, + rule_index_map: BTreeMap::new(), + rule_worklist: Vec::new(), + rule_definitions: Vec::new(), + rule_definition_function_params: Vec::new(), + rule_definition_destructuring_patterns: Vec::new(), + rule_types: Vec::new(), + rule_function_param_count: Vec::new(), + rule_result_registers: Vec::new(), + rule_num_registers: Vec::new(), + context_stack: vec![], + loop_expr_register_map: BTreeMap::new(), + source_to_index: BTreeMap::new(), + builtin_index_map: BTreeMap::new(), + current_input_register: None, + current_data_register: None, + current_rule_path: String::new(), + current_call_stack: Vec::new(), + entry_points: IndexMap::new(), + } + } +} diff --git a/src/languages/rego/compiler/program.rs b/src/languages/rego/compiler/program.rs new file mode 100644 index 0000000..25a56ca --- /dev/null +++ b/src/languages/rego/compiler/program.rs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{Compiler, Result}; +use crate::interpreter::Interpreter; +use crate::rvm::program::{Program, RuleType, SpanInfo}; +use crate::rvm::Instruction; +use crate::Rc; +use crate::Value; +use alloc::collections::BTreeMap; +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec; +use alloc::vec::Vec; + +impl<'a> Compiler<'a> { + pub(super) fn emit_return(&mut self, result_reg: super::Register) { + self.program + .instructions + .push(Instruction::Return { value: result_reg }); + self.spans.push(SpanInfo::new(0, 0, 0, 0)); + } + + pub(super) fn emit_call_rule(&mut self, dest: super::Register, rule_index: u16) { + self.program + .instructions + .push(Instruction::CallRule { dest, rule_index }); + self.spans.push(SpanInfo::new(0, 0, 0, 0)); + } + + pub(super) fn finish(mut self) -> Result { + self.program.main_entry_point = 0; + + self.program.max_rule_window_size = + self.rule_num_registers.iter().cloned().max().unwrap_or(0) as usize; + self.program.dispatch_window_size = self.register_counter as usize; + + let mut rule_infos_map = BTreeMap::new(); + + let function_rule_indices: Vec = self + .rule_index_map + .values() + .copied() + .filter(|&rule_index| self.rule_function_param_count[rule_index as usize].is_some()) + .collect(); + + let mut all_destructuring_blocks = BTreeMap::new(); + for rule_index in function_rule_indices { + let destructuring_blocks = self.extract_destructuring_blocks(rule_index); + all_destructuring_blocks.insert(rule_index, destructuring_blocks); + } + + for (rule_path, &rule_index) in &self.rule_index_map { + let definitions = self.rule_definitions[rule_index as usize].clone(); + let rule_type = self.rule_types[rule_index as usize].clone(); + let function_param_count = &self.rule_function_param_count[rule_index as usize]; + let result_register = self.rule_result_registers[rule_index as usize]; + let num_registers = self.rule_num_registers[rule_index as usize]; + + let destructuring_blocks = all_destructuring_blocks + .get(&rule_index) + .cloned() + .unwrap_or_else(|| vec![None; definitions.len()]); + + let mut rule_info = match function_param_count { + Some(param_count) => { + let definition_params = + &self.rule_definition_function_params[rule_index as usize]; + let param_names = + if let Some(Some(first_def_params)) = definition_params.first() { + first_def_params.clone() + } else { + (0..*param_count).map(|i| format!("param_{}", i)).collect() + }; + + crate::rvm::program::RuleInfo::new_function( + rule_path.clone(), + rule_type, + Rc::new(definitions), + param_names, + result_register, + num_registers, + ) + } + None => crate::rvm::program::RuleInfo::new( + rule_path.clone(), + rule_type, + Rc::new(definitions), + result_register, + num_registers, + ), + }; + + rule_info.destructuring_blocks = destructuring_blocks; + rule_infos_map.insert(rule_index as usize, rule_info); + } + + let rule_paths_to_evaluate: Vec<(String, usize)> = self + .rule_index_map + .iter() + .filter_map(|(rule_path, &rule_index)| { + let rule_type = &self.rule_types[rule_index as usize]; + if *rule_type == RuleType::Complete { + Some((rule_path.clone(), rule_index as usize)) + } else { + None + } + }) + .collect(); + + for (rule_path, rule_index) in rule_paths_to_evaluate { + if let Some(default_literal_index) = self.evaluate_default_rule(&rule_path) { + if let Some(rule_info) = rule_infos_map.get_mut(&rule_index) { + rule_info.set_default_literal_index(default_literal_index); + } + } + } + + self.program.rule_infos = rule_infos_map.into_values().collect(); + + for module in self.policy.get_modules().iter() { + let source = &module.package.refr.span().source; + let source_path = source.get_path().to_string(); + let source_content = source.get_contents().to_string(); + self.program.add_source(source_path, source_content); + } + + self.program.instruction_spans = self.spans.into_iter().map(Some).collect(); + self.program.entry_points = self.entry_points; + + if !self.program.builtin_info_table.is_empty() { + self.program.initialize_resolved_builtins()?; + } + + Ok(self.program) + } + + fn evaluate_default_rule(&mut self, rule_path: &str) -> Option { + if !self.policy.inner.default_rules.contains_key(rule_path) { + return None; + } + + let mut interpreter = Interpreter::new_from_compiled_policy(self.policy.inner.clone()); + + match interpreter.eval_default_rule_for_compiler(rule_path) { + Ok(computed_value) => { + if computed_value != Value::Undefined { + let literal_index = self.add_literal(computed_value); + return Some(literal_index); + } + } + Err(_e) => {} + } + + None + } + + fn extract_destructuring_blocks(&self, rule_index: u16) -> Vec> { + self.rule_definition_destructuring_patterns[rule_index as usize].clone() + } +} diff --git a/src/languages/rego/compiler/queries.rs b/src/languages/rego/compiler/queries.rs new file mode 100644 index 0000000..87831c0 --- /dev/null +++ b/src/languages/rego/compiler/queries.rs @@ -0,0 +1,245 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{Compiler, CompilerError, ComprehensionType, ContextType, Result}; +use crate::ast::{self, LiteralStmt, Query}; +use crate::rvm::program::RuleType; +use crate::rvm::Instruction; +use alloc::vec::Vec; + +impl<'a> Compiler<'a> { + pub(super) fn compile_query(&mut self, query: &Query) -> Result<()> { + self.push_scope(); + + let result = { + let schedule = match &self.policy.inner.schedule { + Some(s) => s.queries.get(self.current_module_index, query.qidx), + None => None, + }; + + let ordered_stmts: Vec<&LiteralStmt> = match schedule { + Some(schedule) => schedule + .order + .iter() + .map(|i| &query.stmts[*i as usize]) + .collect(), + None => query.stmts.iter().collect(), + }; + self.hoist_loops_and_compile_statements(&ordered_stmts) + }; + + self.pop_scope(); + + result + } + + pub(super) fn hoist_loops_and_compile_statements( + &mut self, + stmts: &[&LiteralStmt], + ) -> Result<()> { + for (idx, stmt) in stmts.iter().enumerate() { + let loop_exprs = self.get_statement_loops(stmt)?; + + if !loop_exprs.is_empty() { + return self.compile_hoisted_loops(&stmts[idx..], &loop_exprs); + } + + if matches!(&stmt.literal, ast::Literal::SomeIn { .. }) { + if let ast::Literal::SomeIn { + ref key, + ref value, + ref collection, + .. + } = &stmt.literal + { + self.compile_some_in_loop_with_remaining_statements( + key, + value, + collection, + &stmts[idx..], + )?; + return Ok(()); + } + } + + self.compile_single_statement(stmt)?; + } + + self.hoist_loops_and_emit_context_yield() + } + + pub(super) fn hoist_loops_and_emit_context_yield(&mut self) -> Result<()> { + if let Some(context) = self.context_stack.last() { + match &context.context_type { + ContextType::Every => { + return Ok(()); + } + ContextType::Rule(_) | ContextType::Comprehension(_) => {} + } + } + + let (key_expr, value_expr) = match self.context_stack.last_mut() { + Some(context) => { + if context.key_value_loops_hoisted { + return self.emit_context_yield(); + } + (context.key_expr.clone(), context.value_expr.clone()) + } + None => return Ok(()), + }; + + let mut key_value_loops = Vec::new(); + + if let Some(expr) = key_expr.as_ref() { + key_value_loops.extend(self.get_expr_loops(expr)); + } + + if let Some(expr) = value_expr.as_ref() { + key_value_loops.extend(self.get_expr_loops(expr)); + } + + if !key_value_loops.is_empty() { + if let Some(context) = self.context_stack.last_mut() { + context.key_value_loops_hoisted = true; + } + self.compile_hoisted_loops(&[], &key_value_loops) + } else { + self.emit_context_yield() + } + } + + pub(super) fn emit_context_yield(&mut self) -> Result<()> { + if let Some(context) = self.context_stack.last().cloned() { + let dest_register = context.dest_register; + let span = &context.span; + let value_register = match context.value_expr { + Some(expr) => self.compile_rego_expr(&expr)?, + None => { + let value_reg = self.alloc_register(); + self.emit_instruction( + Instruction::LoadBool { + dest: value_reg, + value: true, + }, + span, + ); + value_reg + } + }; + + let key_register = context + .key_expr + .map(|key_expr| self.compile_rego_expr(&key_expr)) + .unwrap_or(Ok(value_register))?; + + match context.context_type { + ContextType::Comprehension(ComprehensionType::Array) => { + self.emit_instruction( + Instruction::ComprehensionYield { + value_reg: value_register, + key_reg: None, + }, + span, + ); + } + ContextType::Comprehension(ComprehensionType::Set) => { + self.emit_instruction( + Instruction::ComprehensionYield { + value_reg: value_register, + key_reg: None, + }, + span, + ); + } + ContextType::Rule(RuleType::PartialSet) => { + self.emit_instruction( + Instruction::SetAdd { + set: dest_register, + value: value_register, + }, + span, + ); + } + ContextType::Comprehension(ComprehensionType::Object) => { + self.emit_instruction( + Instruction::ComprehensionYield { + value_reg: value_register, + key_reg: Some(key_register), + }, + span, + ); + } + ContextType::Rule(RuleType::PartialObject) => { + self.emit_instruction( + Instruction::ObjectSet { + obj: dest_register, + key: key_register, + value: value_register, + }, + span, + ); + } + ContextType::Rule(RuleType::Complete) => { + self.emit_instruction( + Instruction::Move { + dest: dest_register, + src: value_register, + }, + span, + ); + } + ContextType::Every => {} + } + Ok(()) + } else { + Err(CompilerError::MissingYieldContext) + } + } + + pub(super) fn compile_single_statement(&mut self, stmt: &LiteralStmt) -> Result<()> { + match &stmt.literal { + ast::Literal::Expr { expr, .. } => { + let assert_condition = !matches!(expr.as_ref(), ast::Expr::AssignExpr { .. }); + let _condition_reg = + self.compile_rego_expr_with_span(expr, &stmt.span, assert_condition)?; + } + ast::Literal::SomeIn { .. } => { + return Err(CompilerError::SomeInNotHoisted); + } + ast::Literal::Every { + key, + value, + domain, + query, + .. + } => { + self.compile_every_quantifier(key, value, domain, query, &stmt.span)?; + } + ast::Literal::SomeVars { vars, .. } => { + for var in vars { + self.add_unbound_variable(var.text()); + } + } + ast::Literal::NotExpr { expr, .. } => { + let expr_reg = self.compile_rego_expr_with_span(expr, expr.span(), false)?; + + let negated_reg = self.alloc_register(); + self.emit_instruction( + Instruction::Not { + dest: negated_reg, + operand: expr_reg, + }, + &stmt.span, + ); + + self.emit_instruction( + Instruction::AssertCondition { + condition: negated_reg, + }, + &stmt.span, + ); + } + } + Ok(()) + } +} diff --git a/src/languages/rego/compiler/references.rs b/src/languages/rego/compiler/references.rs new file mode 100644 index 0000000..fbc6989 --- /dev/null +++ b/src/languages/rego/compiler/references.rs @@ -0,0 +1,431 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{Compiler, CompilerError, Register, Result, WorklistEntry}; +use crate::lexer::Span; +use crate::rvm::instructions::{ + ChainedIndexParams, LiteralOrRegister, VirtualDataDocumentLookupParams, +}; +use crate::rvm::Instruction; +use crate::Value; +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec; +use alloc::vec::Vec; + +use crate::ast::{Expr, ExprRef}; + +/// Component of a reference chain - either a literal field or a dynamic expression +#[derive(Debug, Clone)] +pub(super) enum AccessComponent { + /// Static field access (e.g., .field_name) + Field(String), + /// Dynamic access (e.g., [expr]) + Expression(ExprRef), +} + +/// Represents a chained reference like data.a.b[expr].c[expr] +#[derive(Debug, Clone)] +pub(super) struct ReferenceChain { + /// The root variable (e.g., "data", "input", "local_var") + pub(super) root: String, + /// Chain of field accesses - either literal field names or dynamic expressions + pub(super) components: Vec, +} + +impl ReferenceChain { + /// Get the static prefix path (all literal components from the start) + pub(super) fn get_static_prefix(&self) -> Vec<&str> { + let mut prefix = vec![self.root.as_str()]; + for component in &self.components { + match component { + AccessComponent::Field(field) => prefix.push(field.as_str()), + AccessComponent::Expression(_) => break, + } + } + prefix + } +} + +/// Parse a chained reference expression into a ReferenceChain +pub(super) fn parse_reference_chain(expr: &ExprRef) -> Result { + let mut components = Vec::new(); + let mut current_expr = expr; + + // Walk the chain backwards to collect components + loop { + match current_expr.as_ref() { + Expr::Var { span, .. } => { + // Found the root variable + let root = span.text().to_string(); + components.reverse(); // We built backwards, so reverse + return Ok(ReferenceChain { root, components }); + } + Expr::RefDot { refr, field, .. } => { + let (span, _) = field; + components.push(AccessComponent::Field(span.text().to_string())); + current_expr = refr; + } + Expr::RefBrack { refr, index, .. } => { + // Bracket access - check if it's a string literal or dynamic + match index.as_ref() { + Expr::String { span, .. } => { + // String literal - treat as static field + components.push(AccessComponent::Field(span.text().to_string())); + } + _ => { + // Dynamic expression + components.push(AccessComponent::Expression(index.clone())); + } + } + current_expr = refr; + } + _ => { + return Err(CompilerError::NotSimpleReferenceChain); + } + } + } +} + +impl<'a> Compiler<'a> { + /// Compile chained reference expressions (Var, RefDot, RefBrack chains) + /// Uses ReferenceChain to analyze and optimize the access pattern + pub(super) fn compile_chained_ref(&mut self, expr: &ExprRef, span: &Span) -> Result { + // Parse the expression into a reference chain + let chain = parse_reference_chain(expr)?; + + match chain.root.as_str() { + "input" => self.compile_input_chain(&chain, span), + "data" => self.compile_data_chain(&chain, span), + _ => self.compile_local_var_chain(&chain, span), + } + } + + /// Compile input variable access chain + fn compile_input_chain(&mut self, chain: &ReferenceChain, span: &Span) -> Result { + let input_reg = self.resolve_variable("input", span)?; + + if chain.components.is_empty() { + // Just "input" + return Ok(input_reg); + } + + self.compile_chain_access(input_reg, &chain.components, span) + } + + /// Compile data namespace access chain (may involve rules) + fn compile_data_chain(&mut self, chain: &ReferenceChain, span: &Span) -> Result { + if chain.components.is_empty() { + // Just "data" - direct access to data root is not allowed + return Err(CompilerError::DirectDataAccess); + } + + // Build the static prefix path components for rule matching + let static_prefix = chain.get_static_prefix(); + + // Try to find the longest matching rule prefix + // Start from the full path and work backwards + for i in (1..static_prefix.len()).rev() { + // Start from 1 to skip just "data" + let rule_candidate = static_prefix[0..=i].join("."); + + if let Ok(rule_index) = self.get_or_assign_rule_index(&rule_candidate) { + // Found a rule match! Call the rule + let rule_result_reg = self.alloc_register(); + self.emit_instruction( + Instruction::CallRule { + dest: rule_result_reg, + rule_index, + }, + span, + ); + + // Handle remaining components after the matched rule + let consumed_components = i; + if consumed_components < chain.components.len() { + let remaining_components = &chain.components[consumed_components..]; + return self.compile_chain_access(rule_result_reg, remaining_components, span); + } + + return Ok(rule_result_reg); + } + } + + // Check if this path could be a prefix of any rules (for virtual document lookup) + // Convert the full chain to a pattern that includes wildcards for dynamic components + let path_pattern = self.create_path_pattern(&chain.components); + let matching_rules: Vec = self + .policy + .inner + .rules + .keys() + .filter(|rule_path| self.matches_path_pattern(rule_path, &path_pattern)) + .cloned() + .collect(); + + if !matching_rules.is_empty() { + // This path is a prefix of some rules - use DataVirtualDocumentLookup + for rule_path in &matching_rules { + if !self + .rule_worklist + .iter() + .any(|entry| entry.rule_path == *rule_path) + { + // Assign a rule index for this rule before adding to worklist + self.get_or_assign_rule_index(rule_path)?; + let entry = + WorklistEntry::new(rule_path.clone(), self.current_call_stack.clone()); + self.rule_worklist.push(entry); + } + } + + return self.compile_data_virtual_lookup(&chain.components, span); + } + + // No rules involved - simple data access + let data_reg = self.resolve_variable("data", span)?; + self.compile_chain_access(data_reg, &chain.components, span) + } + + /// Create a path pattern from access components, using '*' for dynamic components + /// e.g., [Field("a"), Expression(...), Field("b")] becomes "data.a.*.b" + fn create_path_pattern(&self, components: &[AccessComponent]) -> String { + let mut pattern_parts = vec!["data"]; + + for component in components { + match component { + AccessComponent::Field(field) => pattern_parts.push(field.as_str()), + AccessComponent::Expression(_) => pattern_parts.push("*"), + } + } + + pattern_parts.join(".") + } + + /// Check if a rule path matches the given pattern with wildcards + /// e.g., "data.test.users.alice_profile" matches "data.test.users.*" + fn matches_path_pattern(&self, rule_path: &str, pattern: &str) -> bool { + // Use simple string matching implementation that handles wildcard patterns + if pattern.contains('*') { + self.simple_wildcard_match(rule_path, pattern) + } else { + // Simple prefix match for patterns without wildcards + rule_path.starts_with(&format!("{}.", pattern)) || rule_path == pattern + } + } + + /// Simple wildcard matching without regex dependencies + /// Checks if a rule path could be a prefix of the access pattern + /// e.g., rule "data.test.users.alice_data" matches pattern "data.*.*.*.*.* because + /// the rule could be accessed with the first 4 components of the pattern + /// Ensures exact component matching - "fee" will NOT match "feed" + fn simple_wildcard_match(&self, rule_path: &str, access_pattern: &str) -> bool { + let rule_parts: Vec<&str> = rule_path.split('.').collect(); + let pattern_parts: Vec<&str> = access_pattern.split('.').collect(); + + // Check how many components of the pattern the rule can match + let match_length = rule_parts.len().min(pattern_parts.len()); + + // Check if the rule matches the pattern up to the available components + for i in 0..match_length { + let rule_part = rule_parts[i]; + let pattern_part = pattern_parts[i]; + + if pattern_part == "*" { + // Wildcard in pattern matches any non-empty rule component exactly + if rule_part.is_empty() { + return false; + } + } else if rule_part != pattern_part { + return false; + } + } + + // Rule matches if either: + // 1. It's at least as long as the pattern, OR + // 2. It matches all available components and the remaining pattern parts are wildcards + rule_parts.len() >= pattern_parts.len() + || (match_length > 0 + && pattern_parts[match_length..] + .iter() + .all(|&part| part == "*")) + } + + /// Compile local variable access chain + fn compile_local_var_chain(&mut self, chain: &ReferenceChain, span: &Span) -> Result { + // Check if it's a local variable first (precedence over rules) + if let Some(var_reg) = self.lookup_variable(&chain.root) { + if chain.components.is_empty() { + return Ok(var_reg); + } + return self.compile_chain_access(var_reg, &chain.components, span); + } + + // Check if there's a rule in the current package that matches + let current_pkg_prefix = format!("{}.{}", &self.current_package, &chain.root); + + // Build static path for rule matching + let mut rule_path_parts = vec![current_pkg_prefix.as_str()]; + for component in &chain.components { + match component { + AccessComponent::Field(field) => rule_path_parts.push(field.as_str()), + AccessComponent::Expression(_) => break, // Stop at first dynamic component + } + } + + // Try to find the longest matching rule prefix + for i in (0..rule_path_parts.len()).rev() { + let rule_candidate = rule_path_parts[0..=i].join("."); + + if let Ok(rule_index) = self.get_or_assign_rule_index(&rule_candidate) { + let rule_result_reg = self.alloc_register(); + self.emit_instruction( + Instruction::CallRule { + dest: rule_result_reg, + rule_index, + }, + span, + ); + + // Handle remaining components after the matched rule + let consumed_components = i; // Number of components consumed by the rule (excluding root) + if consumed_components < chain.components.len() { + let remaining_components = &chain.components[consumed_components..]; + return self.compile_chain_access(rule_result_reg, remaining_components, span); + } + + return Ok(rule_result_reg); + } + } + + // No rule found - undefined variable + Err(CompilerError::UndefinedVariable { + name: chain.root.clone(), + }) + } + + /// Compile chain access using appropriate instructions based on chain length and complexity + fn compile_chain_access( + &mut self, + root_reg: Register, + components: &[AccessComponent], + span: &Span, + ) -> Result { + if components.is_empty() { + return Ok(root_reg); + } + + if components.len() == 1 { + // Single level access - use optimized instructions + match &components[0] { + AccessComponent::Field(field) => { + let dest_reg = self.alloc_register(); + let literal_idx = self.add_literal(Value::String(field.clone().into())); + self.emit_instruction( + Instruction::IndexLiteral { + dest: dest_reg, + container: root_reg, + literal_idx, + }, + span, + ); + Ok(dest_reg) + } + AccessComponent::Expression(expr) => { + let dest_reg = self.alloc_register(); + let key_reg = self.compile_rego_expr_with_span(expr, expr.span(), false)?; + self.emit_instruction( + Instruction::Index { + dest: dest_reg, + container: root_reg, + key: key_reg, + }, + span, + ); + Ok(dest_reg) + } + } + } else { + // Multi-level access - use ChainedIndex + let dest_reg = self.alloc_register(); + let mut path_components = Vec::new(); + + for component in components { + match component { + AccessComponent::Field(field) => { + let literal_idx = self.add_literal(Value::String(field.clone().into())); + path_components.push(LiteralOrRegister::Literal(literal_idx)); + } + AccessComponent::Expression(expr) => { + let reg = self.compile_rego_expr_with_span(expr, expr.span(), false)?; + path_components.push(LiteralOrRegister::Register(reg)); + } + } + } + + let params = ChainedIndexParams { + dest: dest_reg, + root: root_reg, + path_components, + }; + + let params_index = self.program.instruction_data.chained_index_params.len() as u16; + self.program + .instruction_data + .chained_index_params + .push(params); + + self.emit_instruction(Instruction::ChainedIndex { params_index }, span); + + Ok(dest_reg) + } + } + + /// Compile data virtual document lookup for rule-involved data access + fn compile_data_virtual_lookup( + &mut self, + components: &[AccessComponent], + span: &Span, + ) -> Result { + let dest_reg = self.alloc_register(); + let mut path_components = Vec::new(); + + for component in components { + match component { + AccessComponent::Field(field) => { + let literal_idx = self.add_literal(Value::String(field.clone().into())); + path_components.push(LiteralOrRegister::Literal(literal_idx)); + } + AccessComponent::Expression(expr) => { + let reg = self.compile_rego_expr_with_span(expr, expr.span(), false)?; + path_components.push(LiteralOrRegister::Register(reg)); + } + } + } + + let params = VirtualDataDocumentLookupParams { + dest: dest_reg, + path_components, + }; + + let params_index = self + .program + .instruction_data + .virtual_data_document_lookup_params + .len() as u16; + self.program + .instruction_data + .virtual_data_document_lookup_params + .push(params); + + self.emit_instruction( + Instruction::VirtualDataDocumentLookup { params_index }, + span, + ); + + // Set flag indicating runtime recursion check is needed + self.program.needs_runtime_recursion_check = true; + + Ok(dest_reg) + } +} diff --git a/src/languages/rego/compiler/rules.rs b/src/languages/rego/compiler/rules.rs new file mode 100644 index 0000000..11340df --- /dev/null +++ b/src/languages/rego/compiler/rules.rs @@ -0,0 +1,529 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{CompilationContext, Compiler, CompilerError, ContextType, Result, WorklistEntry}; +use crate::ast::{Expr, Rule, RuleHead}; +use crate::compiler::destructuring_planner::plans::BindingPlan; +use crate::lexer::Span; +use crate::rvm::program::{Program, RuleType}; +use crate::rvm::Instruction; +use crate::utils::get_path_string; +use crate::Map; +use crate::{CompiledPolicy, Value}; +use alloc::collections::BTreeSet; +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::sync::Arc; +use alloc::vec::Vec; + +impl<'a> Compiler<'a> { + pub(super) fn compute_rule_type(&self, rule_path: &str) -> Result { + let Some(definitions) = self.policy.inner.rules.get(rule_path) else { + return Err(CompilerError::General { + message: format!("no definitions found for rule path '{}'", rule_path), + }); + }; + + let rule_types: BTreeSet = definitions + .iter() + .map(|def| { + if let Rule::Spec { head, .. } = def.as_ref() { + match head { + RuleHead::Set { .. } => RuleType::PartialSet, + RuleHead::Compr { refr, assign, .. } => match refr.as_ref() { + crate::ast::Expr::RefBrack { .. } if assign.is_some() => { + RuleType::PartialObject + } + crate::ast::Expr::RefBrack { .. } => RuleType::PartialSet, + _ => RuleType::Complete, + }, + _ => RuleType::Complete, + } + } else { + RuleType::Complete + } + }) + .collect(); + + if rule_types.len() > 1 { + return Err(CompilerError::General { + message: format!( + "internal: rule '{}' has multiple types: {:?}", + rule_path, rule_types + ), + }); + } + + rule_types + .into_iter() + .next() + .ok_or_else(|| CompilerError::RuleTypeNotFound { + rule_path: rule_path.to_string(), + }) + } + + pub(super) fn get_or_assign_rule_index(&mut self, rule_path: &str) -> Result { + if let Some(&index) = self.rule_index_map.get(rule_path) { + return Ok(index); + } + + let rule_type = self.compute_rule_type(rule_path)?; + let index = self.rule_index_map.len() as u16; + + self.rule_index_map.insert(rule_path.to_string(), index); + let entry = WorklistEntry::new(rule_path.to_string(), self.current_call_stack.clone()); + self.rule_worklist.push(entry); + + while self.rule_definitions.len() <= index as usize { + self.rule_definitions.push(Vec::new()); + } + + while self.rule_types.len() <= index as usize { + self.rule_types.push(RuleType::Complete); + } + self.rule_types[index as usize] = rule_type; + + while self.rule_definition_function_params.len() <= index as usize { + self.rule_definition_function_params.push(Vec::new()); + } + + while self.rule_definition_destructuring_patterns.len() <= index as usize { + self.rule_definition_destructuring_patterns.push(Vec::new()); + } + + while self.rule_function_param_count.len() <= index as usize { + self.rule_function_param_count.push(None); + } + + while self.rule_result_registers.len() <= index as usize { + self.rule_result_registers.push(0); + } + + Ok(index) + } + + fn find_module_index_for_rule(&self, rule_ref: &crate::ast::NodeRef) -> Result { + let rule_ptr = rule_ref.as_ref() as *const Rule; + + for (module_idx, module) in self.policy.get_modules().iter().enumerate() { + for policy_rule in &module.policy { + let policy_rule_ptr = policy_rule.as_ref() as *const Rule; + if policy_rule_ptr == rule_ptr { + return Ok(module_idx as u32); + } + } + } + + Ok(0) + } + + fn find_module_package_and_index_for_rule( + &self, + rule_path: &str, + rules: &Map>>, + ) -> Result<(String, u32)> { + if let Some(rule_definitions) = rules.get(rule_path) { + if let Some(first_rule_ref) = rule_definitions.first() { + let rule_ptr = first_rule_ref.as_ref() as *const Rule; + + for (module_index, module) in self.policy.get_modules().iter().enumerate() { + for policy_rule in &module.policy { + let policy_rule_ptr = policy_rule.as_ref() as *const Rule; + if policy_rule_ptr == rule_ptr { + let package_path = get_path_string(&module.package.refr, Some("data")) + .map_err(|e| CompilerError::General { + message: format!( + "Failed to get package path for module: {}", + e + ), + })?; + return Ok((package_path, module_index as u32)); + } + } + } + } + } + + let package = if let Some(last_dot) = rule_path.rfind('.') { + rule_path[..last_dot].to_string() + } else { + "data".to_string() + }; + Ok((package, 0)) + } + + /// Compile from a CompiledPolicy to RVM Program + pub fn compile_from_policy( + policy: &CompiledPolicy, + entry_points: &[&str], + ) -> Result> { + let mut compiler = Compiler::with_policy(policy); + compiler.current_rule_path = "".to_string(); + let rules = policy.get_rules(); + + for &entry_point_name in entry_points { + let instruction_index = compiler.program.instructions.len(); + let result_reg = compiler.alloc_register(); + let rule_idx = compiler.get_or_assign_rule_index(entry_point_name)?; + compiler + .entry_points + .insert(entry_point_name.to_string(), instruction_index); + compiler.emit_call_rule(result_reg, rule_idx); + + compiler.emit_return(result_reg); + } + + compiler.compile_worklist_rules(rules)?; + + let program = Arc::new(compiler.finish()?); + Ok(program) + } + + fn compile_worklist_rules( + &mut self, + rules: &Map>>, + ) -> Result<()> { + let mut compiled_rules = BTreeSet::new(); + let mut call_stack = Vec::new(); + + while !self.rule_worklist.is_empty() { + let entry = self.rule_worklist.remove(0); + + if let Some(&target_rule_index) = self.rule_index_map.get(&entry.rule_path) { + if entry.call_stack.contains(&target_rule_index) { + let mut chain = Vec::new(); + let mut found_start = false; + for &rule_idx in &entry.call_stack { + if rule_idx == target_rule_index { + found_start = true; + } + if found_start { + if let Some((rule_path, _)) = + self.rule_index_map.iter().find(|(_, &idx)| idx == rule_idx) + { + chain.push(rule_path.clone()); + } + } + } + chain.push(entry.rule_path.clone()); + + return Err(CompilerError::General { + message: format!( + "Compile-time recursion detected in rule call chain: {}", + chain.join(" -> ") + ), + }); + } + } + + if compiled_rules.contains(&entry.rule_path) { + continue; + } + + let rule_index = if let Some(&index) = self.rule_index_map.get(&entry.rule_path) { + index + } else { + return Err(CompilerError::General { + message: format!("Rule index not found for '{}'", entry.rule_path), + }); + }; + + call_stack.push(entry.rule_path.clone()); + + let old_rule_path = self.current_rule_path.clone(); + let old_call_stack = self.current_call_stack.clone(); + self.current_rule_path = entry.rule_path.clone(); + self.current_call_stack = entry.call_stack.clone(); + self.current_call_stack.push(rule_index); + + let result = self.compile_worklist_rule(&entry.rule_path, rules); + + self.current_rule_path = old_rule_path; + self.current_call_stack = old_call_stack; + + call_stack.pop(); + + result?; + compiled_rules.insert(entry.rule_path); + } + Ok(()) + } + + fn compile_worklist_rule( + &mut self, + rule_path: &str, + rules: &Map>>, + ) -> Result<()> { + let (module_package, module_index) = + self.find_module_package_and_index_for_rule(rule_path, rules)?; + + let saved_package = self.current_package.clone(); + let saved_module_index = self.current_module_index; + self.current_package = module_package.clone(); + self.current_module_index = module_index; + + let saved_register_counter = self.register_counter; + if let Some(rule_definitions) = rules.get(rule_path) { + let rule_index = self.rule_index_map.get(rule_path).copied().ok_or_else(|| { + CompilerError::General { + message: format!( + "Rule '{}' not found in rule index map during compilation", + rule_path + ), + } + })?; + let rule_type = self.rule_types[rule_index as usize].clone(); + + let result_register = 0; + + while self.rule_result_registers.len() <= rule_index as usize { + self.rule_result_registers.push(0); + } + self.rule_result_registers[rule_index as usize] = result_register; + + while self.rule_definitions.len() <= rule_index as usize { + self.rule_definitions.push(Vec::new()); + } + + while self.rule_definition_function_params.len() <= rule_index as usize { + self.rule_definition_function_params.push(Vec::new()); + } + + while self.rule_definition_destructuring_patterns.len() <= rule_index as usize { + self.rule_definition_destructuring_patterns.push(Vec::new()); + } + + let mut num_registers_used = 0; + let mut rule_param_count: Option = None; + + for (def_idx, rule_ref) in rule_definitions.iter().enumerate() { + ::core::convert::identity(def_idx); + if let Rule::Spec { head, bodies, span } = rule_ref.as_ref() { + self.push_scope(); + self.register_counter = 0; + + let result_register = self.alloc_register(); + + self.current_module_index = self.find_module_index_for_rule(rule_ref)?; + + let (key_expr, value_expr) = match head { + RuleHead::Compr { refr, assign, .. } => { + self.rule_definition_function_params[rule_index as usize].push(None); + self.rule_definition_destructuring_patterns[rule_index as usize] + .push(None); + + let output_expr = assign.as_ref().map(|assign| assign.value.clone()); + let key_expr = match refr.as_ref() { + Expr::RefBrack { index, .. } => Some(index.clone()), + _ => None, + }; + (key_expr, output_expr) + } + RuleHead::Set { key, .. } => { + self.rule_definition_function_params[rule_index as usize].push(None); + self.rule_definition_destructuring_patterns[rule_index as usize] + .push(None); + + (None, key.clone()) + } + RuleHead::Func { assign, args, .. } => { + let mut param_names = Vec::new(); + let mut last_param_span: Option = None; + + let destructuring_entry = if args.is_empty() { + None + } else { + Some(self.program.instructions.len()) + }; + + let param_base_register = self.register_counter; + self.register_counter = + self.register_counter.saturating_add(args.len() as u8); + + for (arg_idx, arg) in args.iter().enumerate() { + let param_reg = param_base_register + arg_idx as u8; + + let param_name = match arg.as_ref() { + Expr::Var { + value: Value::String(name), + .. + } => name.to_string(), + _ => format!("__param_{}", arg_idx), + }; + param_names.push(param_name); + + let context_desc = format!("function parameter {arg_idx}"); + let binding_plan = + self.expect_binding_plan_for_expr(arg, &context_desc)?; + + if let BindingPlan::Parameter { .. } = &binding_plan { + self.apply_binding_plan(&binding_plan, param_reg, arg.span()) + .map_err(CompilerError::from)?; + } else { + return Err(CompilerError::UnexpectedBindingPlan { + context: context_desc, + found: format!("{binding_plan:?}"), + }); + } + + last_param_span = Some(arg.span().clone()); + } + + self.rule_definition_function_params[rule_index as usize] + .push(Some(param_names.clone())); + + if let Some(entry) = destructuring_entry { + let success_span = last_param_span.as_ref().unwrap_or(span); + self.emit_instruction( + crate::rvm::instructions::Instruction::DestructuringSuccess {}, + success_span, + ); + self.rule_definition_destructuring_patterns[rule_index as usize] + .push(Some(entry as u32)); + } else { + self.rule_definition_destructuring_patterns[rule_index as usize] + .push(None); + } + + match rule_param_count { + None => { + rule_param_count = Some(param_names.len()); + } + Some(expected_count) => { + if param_names.len() != expected_count { + return Err(CompilerError::General { + message: format!( + "Function rule '{}' definition {} has {} parameters but expected {} parameters", + rule_path, def_idx, param_names.len(), expected_count + ), + }); + } + } + } + + match assign { + Some(assignment) => (None, Some(assignment.value.clone())), + None => (None, None), + } + } + }; + + let span = match (&key_expr, &value_expr) { + (_, Some(expr)) => expr.span().clone(), + (Some(expr), _) => expr.span().clone(), + _ => span.clone(), + }; + + let context = CompilationContext { + dest_register: result_register, + context_type: ContextType::Rule(rule_type.clone()), + key_expr, + value_expr, + span, + key_value_loops_hoisted: false, + }; + self.push_context(context); + let mut body_entry_points = Vec::new(); + + if bodies.is_empty() { + let value_expr_opt = self.context_stack.last().unwrap().value_expr.clone(); + if let Some(value_expr) = value_expr_opt { + let body_entry_point = self.program.instructions.len() as u32; + body_entry_points.push(body_entry_point); + + self.push_scope(); + self.reset_rule_definition_registers(); + + self.emit_instruction( + Instruction::RuleInit { + result_reg: result_register, + rule_index, + }, + value_expr.span(), + ); + + self.emit_context_yield()?; + + self.emit_instruction(Instruction::RuleReturn {}, value_expr.span()); + self.pop_scope(); + } + } else { + for (body_idx, body) in bodies.iter().enumerate() { + self.push_scope(); + self.reset_rule_definition_registers(); + + let body_entry_point = self.program.instructions.len() as u32; + body_entry_points.push(body_entry_point); + + ::core::convert::identity(body_idx); + + self.emit_instruction( + Instruction::RuleInit { + result_reg: result_register, + rule_index, + }, + &body.span, + ); + + if !body.query.stmts.is_empty() { + self.compile_query(&body.query)?; + } else { + let value_expr_opt = + self.context_stack.last().unwrap().value_expr.clone(); + if let Some(value_expr) = value_expr_opt { + let value_reg = self.compile_rego_expr(&value_expr)?; + self.emit_instruction( + Instruction::Move { + dest: result_register, + src: value_reg, + }, + value_expr.span(), + ); + } + } + + self.emit_instruction(Instruction::RuleReturn {}, &body.span); + + self.pop_scope(); + } + } + + self.pop_scope(); + + self.rule_definitions[rule_index as usize].push(body_entry_points); + + if self.register_counter > num_registers_used { + num_registers_used = self.register_counter; + } + } + } + + while self.rule_num_registers.len() <= rule_index as usize { + self.rule_num_registers.push(0); + } + self.rule_num_registers[rule_index as usize] = num_registers_used; + + self.rule_function_param_count[rule_index as usize] = rule_param_count; + + if rule_param_count.is_none() { + let rule_path_parts: Vec<&str> = rule_path.split('.').collect(); + if let Some((rule_name, package_parts)) = rule_path_parts.split_last() { + let package_path: Vec = + package_parts.iter().map(|s| s.to_string()).collect(); + + let _ = self.program.add_rule_to_tree( + &package_path, + rule_name, + rule_index as usize, + ); + } + } + + self.register_counter = saved_register_counter; + self.current_package = saved_package; + self.current_module_index = saved_module_index; + } + + Ok(()) + } +} diff --git a/src/languages/rego/mod.rs b/src/languages/rego/mod.rs new file mode 100644 index 0000000..1bb3009 --- /dev/null +++ b/src/languages/rego/mod.rs @@ -0,0 +1,2 @@ +#[path = "compiler/mod.rs"] +pub mod compiler; diff --git a/src/lib.rs b/src/lib.rs index 11b8d01..bc89bec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,9 @@ mod interpreter; pub mod languages { #[cfg(feature = "azure-rbac")] pub mod azure_rbac; + + #[cfg(feature = "rvm")] + pub mod rego; } mod lexer; @@ -51,6 +54,8 @@ mod scheduler; mod schema; #[cfg(feature = "azure_policy")] pub mod target; +#[cfg(any(test, all(feature = "yaml", feature = "std")))] +pub mod test_utils; mod utils; mod value; diff --git a/src/rvm/tests/vm.rs b/src/rvm/tests/vm.rs index 7c78570..3cb54b2 100644 --- a/src/rvm/tests/vm.rs +++ b/src/rvm/tests/vm.rs @@ -24,7 +24,7 @@ mod tests { } use crate::rvm::vm::{ExecutionMode, ExecutionState, RegoVM, SuspendReason, VmError}; - use crate::tests::interpreter::process_value; + use crate::test_utils::process_value; use crate::value::Value; use alloc::collections::{BTreeMap, VecDeque}; use alloc::string::{String, ToString}; diff --git a/src/test_utils.rs b/src/test_utils.rs new file mode 100644 index 0000000..f408e35 --- /dev/null +++ b/src/test_utils.rs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Shared helpers for YAML-driven integration tests. + +use crate::Value; +use alloc::{vec, vec::Vec}; +use anyhow::{bail, Result}; +use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; + +/// Support single or multiple values inside YAML fixtures. +#[derive(PartialEq, Debug, Clone)] +pub enum ValueOrVec { + Single(Value), + Many(Vec), +} + +impl Serialize for ValueOrVec { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + ValueOrVec::Single(value) => value.serialize(serializer), + ValueOrVec::Many(v) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("many!", v)?; + map.end() + } + } + } +} + +impl<'de> Deserialize<'de> for ValueOrVec { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Value::deserialize(deserializer)?; + + match &value["many!"] { + Value::Array(arr) => Ok(ValueOrVec::Many(arr.to_vec())), + _ => Ok(ValueOrVec::Single(value)), + } + } +} + +/// Convert any YAML-described value into an engine `Value`, handling helper encodings. +pub fn process_value(v: &Value) -> Result { + match v { + Value::String(s) if s.as_ref() == "#undefined" => Ok(Value::Undefined), + Value::Object(ref fields) if fields.len() == 1 && matches!(&v["set!"], Value::Array(_)) => { + let mut set_value = Value::new_set(); + let set = set_value.as_set_mut()?; + for item in v["set!"].as_array()? { + set.insert(process_value(item)?); + } + Ok(set_value) + } + Value::Object(fields) if fields.len() == 1 && matches!(&v["object!"], Value::Array(_)) => { + let mut object_value = Value::new_object(); + let object = object_value.as_object_mut()?; + for item in v["object!"].as_array()? { + let key = process_value(&item["key"])?; + let value = process_value(&item["value"])?; + object.insert(key, value); + } + Ok(object_value) + } + Value::Array(items) => { + let mut array_value = Value::new_array(); + let array = array_value.as_array_mut()?; + for item in items.iter() { + array.push(process_value(item)?); + } + Ok(array_value) + } + Value::Object(fields) => { + let mut object_value = Value::new_object(); + let object = object_value.as_object_mut()?; + for (key, value) in fields.iter() { + object.insert(process_value(key)?, process_value(value)?); + } + Ok(object_value) + } + Value::Set(_) => bail!("unexpected set in value read from json/yaml"), + _ => Ok(v.clone()), + } +} + +/// Diff-friendly equality helper used by multiple YAML suites. +pub fn match_values(computed: &Value, expected: &Value) -> Result<()> { + if computed != expected { + let expected_yaml = serde_yaml::to_string(expected)?; + let computed_yaml = serde_yaml::to_string(computed)?; + bail!("expected:\n{}computed:\n{}", expected_yaml, computed_yaml); + } + Ok(()) +} + +/// Compare two result sets after normalizing special encodings. +pub fn check_output(computed_results: &[Value], expected_results: &[Value]) -> Result<()> { + if computed_results.len() != expected_results.len() { + bail!( + "the number of computed results ({}) and expected results ({}) is not equal", + computed_results.len(), + expected_results.len() + ); + } + + for (n, expected_result) in expected_results.iter().enumerate() { + let expected = process_value(expected_result)?; + if let Some(computed_result) = computed_results.get(n) { + match_values(computed_result, &expected)?; + } + } + + Ok(()) +} + +/// Normalise helper enum to plain vectors for downstream assertions. +pub fn value_or_vec_to_vec(value_or_vec: ValueOrVec) -> Vec { + match value_or_vec { + ValueOrVec::Single(single_result) => vec![single_result], + ValueOrVec::Many(many_result) => many_result, + } +} diff --git a/src/tests/common.rs b/src/tests/common.rs new file mode 100644 index 0000000..c52726a --- /dev/null +++ b/src/tests/common.rs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Shared test utilities for YAML-based test cases + +use crate::*; +use anyhow::{bail, Result}; +use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; + +/// Process test value specified in json/yaml to interpret special encodings. +pub fn process_value(v: &Value) -> Result { + match v { + // Handle Undefined encoded as a string "#undefined" + Value::String(s) if s.as_ref() == "#undefined" => Ok(Value::Undefined), + + // Handle set encoded as an object + // set! : + // - item1 + // - item2 + // ... + Value::Object(ref fields) if fields.len() == 1 && matches!(&v["set!"], Value::Array(_)) => { + let mut set_value = Value::new_set(); + let set = set_value.as_set_mut()?; + for item in v["set!"].as_array()? { + set.insert(process_value(item)?); + } + Ok(set_value) + } + + // Handle complex object specified explicitly: + // object! : + // - key: ... + // value: ... + Value::Object(fields) if fields.len() == 1 && matches!(&v["object!"], Value::Array(_)) => { + let mut object_value = Value::new_object(); + let object = object_value.as_object_mut()?; + for item in v["object!"].as_array()? { + object.insert(process_value(&item["key"])?, process_value(&item["value"])?); + } + Ok(object_value) + } + + // Recursively process arrays + Value::Array(items) => { + let mut array_value = Value::new_array(); + let array = array_value.as_array_mut()?; + for item in items.iter() { + array.push(process_value(item)?); + } + Ok(array_value) + } + + // Recursively process objects + Value::Object(fields) => { + let mut object_value = Value::new_object(); + let object = object_value.as_object_mut()?; + for (key, value) in fields.iter() { + object.insert(process_value(key)?, process_value(value)?); + } + Ok(object_value) + } + + Value::Set(_) => bail!("unexpected set in value read from json/yaml"), + + // Simple variants + _ => Ok(v.clone()), + } +} + +/// Match computed and expected values with pretty diff output +pub fn match_values(computed: &Value, expected: &Value) -> Result<()> { + if computed != expected { + panic!( + "Values do not match:\nExpected: {:?}\nActual: {:?}", + expected, computed + ); + } + Ok(()) +} + +/// Check output results against expected results +pub fn check_output(computed_results: &[Value], expected_results: &[Value]) -> Result<()> { + if computed_results.len() != expected_results.len() { + bail!( + "the number of computed results ({}) and expected results ({}) is not equal", + computed_results.len(), + expected_results.len() + ); + } + + for (n, expected_result) in expected_results.iter().enumerate() { + let expected = match process_value(expected_result) { + Ok(e) => e, + _ => bail!("unable to process value :\n {expected_result:?}"), + }; + + if let Some(computed_result) = computed_results.get(n) { + match match_values(computed_result, &expected) { + Ok(()) => (), + Err(e) => bail!("{e}"), + } + } + } + + Ok(()) +} + +/// Support for single value or multiple values in test input/output +#[derive(PartialEq, Debug, Clone)] +pub enum ValueOrVec { + Single(Value), + Many(Vec), +} + +impl Serialize for ValueOrVec { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + ValueOrVec::Single(value) => value.serialize(serializer), + ValueOrVec::Many(v) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("many!", v)?; + map.end() + } + } + } +} + +impl<'de> Deserialize<'de> for ValueOrVec { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Value::deserialize(deserializer)?; + + match &value["many!"] { + Value::Array(arr) => Ok(ValueOrVec::Many(arr.to_vec())), + _ => Ok(ValueOrVec::Single(value)), + } + } +} + +/// Standard test case structure for YAML tests +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct TestCase { + pub data: Option, + pub input: Option, + pub modules: Vec, + pub note: String, + pub query: String, + pub entry_points: Option>, + pub sort_bindings: Option, + pub want_result: Option, + pub want_results: Option>, + pub want_prints: Option>, + pub no_result: Option, + pub skip: Option, + pub error: Option, + pub traces: Option, + pub want_error: Option, + pub want_error_code: Option, + #[serde(default = "default_strict")] + pub strict: bool, + /// Allow interpreter to succeed when RVM fails with conflict detection + pub allow_interpreter_success: Option, + /// Allow interpreter to produce incorrect results when RVM produces correct results + pub allow_interpreter_incorrect_behavior: Option, +} + +fn default_strict() -> bool { + true +} + +/// Standard YAML test file structure +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct YamlTest { + pub cases: Vec, +} + +/// Convert ValueOrVec to a vector of Values +pub fn value_or_vec_to_vec(value_or_vec: ValueOrVec) -> Vec { + match value_or_vec { + ValueOrVec::Single(single_result) => vec![single_result], + ValueOrVec::Many(many_result) => many_result, + } +} diff --git a/src/tests/interpreter/mod.rs b/src/tests/interpreter/mod.rs index 13d3204..bce2157 100644 --- a/src/tests/interpreter/mod.rs +++ b/src/tests/interpreter/mod.rs @@ -3,10 +3,11 @@ use std::env; +use crate::test_utils::{check_output, ValueOrVec}; use crate::*; use anyhow::{bail, Result}; -use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; use test_generator::test_resources; #[cfg(feature = "azure_policy")] @@ -122,106 +123,6 @@ mod load_target_definitions { } } -// Process test value specified in json/yaml to interpret special encodings. -pub fn process_value(v: &Value) -> Result { - match v { - // Handle Undefined encoded as a string "#undefined" - Value::String(s) if s.as_ref() == "#undefined" => Ok(Value::Undefined), - - // Handle set encoded as an object - // set! : - // - item1 - // - item2 - // ... - Value::Object(ref fields) if fields.len() == 1 && matches!(&v["set!"], Value::Array(_)) => { - let mut set_value = Value::new_set(); - let set = set_value.as_set_mut()?; - for item in v["set!"].as_array()? { - set.insert(process_value(item)?); - } - Ok(set_value) - } - - // Handle complex object specified explicitly: - // object! : - // - key: ... - // value: ... - Value::Object(fields) if fields.len() == 1 && matches!(&v["object!"], Value::Array(_)) => { - let mut object_value = Value::new_object(); - let object = object_value.as_object_mut()?; - for item in v["object!"].as_array()? { - object.insert(process_value(&item["key"])?, process_value(&item["value"])?); - } - Ok(object_value) - } - - // Recursively process arrays - Value::Array(items) => { - let mut array_value = Value::new_array(); - let array = array_value.as_array_mut()?; - for item in items.iter() { - array.push(process_value(item)?); - } - Ok(array_value) - } - - // Recursively process objects - Value::Object(fields) => { - let mut object_value = Value::new_object(); - let object = object_value.as_object_mut()?; - for (key, value) in fields.iter() { - object.insert(process_value(key)?, process_value(value)?); - } - Ok(object_value) - } - - Value::Set(_) => bail!("unexpected set in value read from json/yaml"), - - // Simple variants - _ => Ok(v.clone()), - } -} - -fn match_values(computed: &Value, expected: &Value) -> Result<()> { - if computed != expected { - let expected_yaml = serde_yaml::to_string(&expected)?; - let computed_yaml = serde_yaml::to_string(&computed)?; - panic!( - "expected:\n{}computed:\n{}diff:\n{}", - expected_yaml, - computed_yaml, - prettydiff::diff_chars(&expected_yaml, &computed_yaml) - ); - } - Ok(()) -} - -pub fn check_output(computed_results: &[Value], expected_results: &[Value]) -> Result<()> { - if computed_results.len() != expected_results.len() { - bail!( - "the number of computed results ({}) and expected results ({}) is not equal", - computed_results.len(), - expected_results.len() - ); - } - - for (n, expected_result) in expected_results.iter().enumerate() { - let expected = match process_value(expected_result) { - Ok(e) => e, - _ => bail!("unable to process value :\n {expected_result:?}"), - }; - - if let Some(computed_result) = computed_results.get(n) { - match match_values(computed_result, &expected) { - Ok(()) => (), - Err(e) => bail!("{e}"), - } - } - } - - Ok(()) -} - fn push_query_results(query_results: QueryResults, results: &mut Vec) { if query_results.result.len() == 1 { if let Some(query_result) = query_results.result.last() { @@ -385,42 +286,6 @@ pub fn eval_file_with_rule_evaluation( Ok((results, engine.take_prints()?)) } -#[derive(PartialEq, Debug)] -pub enum ValueOrVec { - Single(Value), - Many(Vec), -} - -impl Serialize for ValueOrVec { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - ValueOrVec::Single(value) => value.serialize(serializer), - ValueOrVec::Many(v) => { - let mut map = serializer.serialize_map(Some(1))?; - map.serialize_entry("many!", v)?; - map.end() - } - } - } -} - -impl<'de> Deserialize<'de> for ValueOrVec { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = Value::deserialize(deserializer)?; - - match &value["many!"] { - Value::Array(arr) => Ok(ValueOrVec::Many(arr.to_vec())), - _ => Ok(ValueOrVec::Single(value)), - } - } -} - #[derive(Serialize, Deserialize, PartialEq, Debug)] struct TestCase { data: Option, diff --git a/tests/mod.rs b/tests/mod.rs index e0a04c4..56086b8 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -8,3 +8,6 @@ mod engine; mod lexer; mod parser; mod value; + +#[cfg(feature = "rvm")] +mod rvm; diff --git a/tests/rvm/mod.rs b/tests/rvm/mod.rs new file mode 100644 index 0000000..d28bff6 --- /dev/null +++ b/tests/rvm/mod.rs @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +mod rego; diff --git a/tests/rvm/rego/cases/arithmetic.yaml b/tests/rvm/rego/cases/arithmetic.yaml new file mode 100644 index 0000000..2f92922 --- /dev/null +++ b/tests/rvm/rego/cases/arithmetic.yaml @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Arithmetic Operations Test Suite +# Tests basic arithmetic operations: addition, multiplication, division, subtraction + +cases: + - note: arithmetic_basic + data: {} + modules: + - | + package test + main := result if { + result := 2 + 3 + } + query: data.test.main + want_result: 5 + + - note: arithmetic_multiply + data: {} + modules: + - | + package test + main := result if { + result := 4 * 6 + } + query: data.test.main + want_result: 24 + + - note: arithmetic_division + data: {} + modules: + - | + package test + main := result if { + result := 15 / 3 + } + query: data.test.main + want_result: 5 + + - note: arithmetic_subtraction + data: {} + modules: + - | + package test + main := result if { + result := 10 - 7 + } + query: data.test.main + want_result: 3 diff --git a/tests/rvm/rego/cases/arrays.yaml b/tests/rvm/rego/cases/arrays.yaml new file mode 100644 index 0000000..5688e84 --- /dev/null +++ b/tests/rvm/rego/cases/arrays.yaml @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Arrays Test Suite +# Tests array creation, nested arrays, indexing, and mixed data structures + +cases: + - note: array_creation + data: {} + modules: + - | + package test + main := result if { + result := [1, 2, 3, "hello", true] + } + query: data.test.main + want_result: [1, 2, 3, "hello", true] + + - note: nested_arrays + data: {} + modules: + - | + package test + main := result if { + result := [[1, 2], [3, 4], ["a", "b"]] + } + query: data.test.main + want_result: [[1, 2], [3, 4], ["a", "b"]] + + - note: array_indexing + data: {} + modules: + - | + package test + arr := [10, 20, 30, 40] + main := result if { + result := arr[2] + } + query: data.test.main + want_result: 30 + + - note: dynamic_array_indexing + data: {} + modules: + - | + package test + arr := ["first", "second", "third"] + index := 1 + main := result if { + result := arr[index] + } + query: data.test.main + want_result: "second" + + - note: mixed_array_object + data: {} + modules: + - | + package test + main := result if { + result := [{"name": "Alice"}, {"name": "Bob"}, [1, 2, 3]] + } + query: data.test.main + want_result: [{"name": "Alice"}, {"name": "Bob"}, [1, 2, 3]] diff --git a/tests/rvm/rego/cases/chained_access.yaml b/tests/rvm/rego/cases/chained_access.yaml new file mode 100644 index 0000000..d615f85 --- /dev/null +++ b/tests/rvm/rego/cases/chained_access.yaml @@ -0,0 +1,293 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Chained Access and Variable Resolution Test Suite +# Tests complex chained reference expressions, dynamic indexing, and variable precedence + +cases: + - note: simple_data_rule_access + data: {} + modules: + - | + package test.users + alice = {"name": "Alice", "age": 30} + - | + package test + main := result if { + result := data.test.users.alice.name + } + query: data.test.main + want_result: "Alice" + + - note: local_variable_precedence_over_rule + data: {} + modules: + - | + package test + alice = {"name": "Global Alice"} + main := result if { + alice := {"name": "Local Alice"} + result := alice.name + } + query: data.test.main + want_result: "Local Alice" + + - note: chained_rule_access_with_fields + data: {} + modules: + - | + package test.auth + user_permissions = { + "alice": {"read": true, "write": false, "admin": false}, + "bob": {"read": true, "write": true, "admin": true} + } + - | + package test + main := result if { + result := data.test.auth.user_permissions.alice.read + } + query: data.test.main + want_result: true + + - note: dynamic_indexing_with_variable + data: {} + modules: + - | + package test.data + users = { + "alice": {"name": "Alice Smith", "role": "user"}, + "bob": {"name": "Bob Jones", "role": "admin"} + } + - | + package test + main := result if { + user_id := "alice" + result := data.test.data.users[user_id].name + } + query: data.test.main + want_result: "Alice Smith" + + - note: mixed_static_and_dynamic_chaining + data: {} + modules: + - | + package test.config + settings = { + "databases": { + "primary": {"host": "db1.example.com", "port": 5432}, + "backup": {"host": "db2.example.com", "port": 5433} + } + } + - | + package test + main := result if { + db_type := "primary" + result := data.test.config.settings.databases[db_type].host + } + query: data.test.main + want_result: "db1.example.com" + + - note: input_field_access + data: {} + modules: + - | + package test + main := result if { + result := input.user.profile.email + } + query: data.test.main + input: {"user": {"profile": {"email": "alice@example.com", "verified": true}}} + want_result: "alice@example.com" + + - note: dynamic_input_access + data: {} + modules: + - | + package test + main := result if { + field := "email" + result := input.user.profile[field] + } + query: data.test.main + input: {"user": {"profile": {"email": "alice@example.com", "phone": "+1234567890"}}} + want_result: "alice@example.com" + + - note: data_document_with_rule_override + data: {"test": {"existing": {"value": "from_data"}}} + modules: + - | + package test.existing + computed = "from_rule" + - | + package test + main := result if { + result := [data.test.existing.value, data.test.existing.computed] + } + query: data.test.main + want_result: ["from_data", "from_rule"] + + - note: longest_rule_prefix_matching + data: {} + modules: + - | + package test.api.v1 + users = ["alice", "bob"] + - | + package test.api.v1.users_pkg + count = 2 + - | + package test + main := result if { + result := [data.test.api.v1.users, data.test.api.v1.users_pkg.count] + } + query: data.test.main + want_result: [["alice", "bob"], 2] + + - note: nested_dynamic_access + data: {} + modules: + - | + package test.complex + matrix = { + "level1": { + "level2a": {"value": "found_a"}, + "level2b": {"value": "found_b"} + } + } + - | + package test + main := result if { + level1_key := "level1" + level2_key := "level2a" + result := data.test.complex.matrix[level1_key][level2_key].value + } + query: data.test.main + want_result: "found_a" + + - note: variable_shadowing_in_chain + data: {} + modules: + - | + package test + config = {"timeout": 30} + main := result if { + config := {"nested": {"timeout": 60}} + result := config.nested.timeout + } + query: data.test.main + want_result: 60 + + - note: array_indexing_in_chain + data: {} + modules: + - | + package test.data + servers = [ + {"name": "web1", "status": "active"}, + {"name": "web2", "status": "inactive"}, + {"name": "db1", "status": "active"} + ] + - | + package test + main := result if { + index := 0 + result := data.test.data.servers[index].name + } + query: data.test.main + want_result: "web1" + + - note: string_literal_bracket_access + data: {} + modules: + - | + package test.metrics + cpu_usage = { + "server-1": 45.2, + "server-2": 78.9, + "load-balancer": 12.3 + } + - | + package test + main := result if { + result := data.test.metrics.cpu_usage["server-1"] + } + query: data.test.main + want_result: 45.2 + + - note: complex_nested_rule_resolution + data: {} + modules: + - | + package test.auth.policies + admin_policy = { + "permissions": ["read", "write", "delete"], + "resources": ["users", "configs", "logs"] + } + - | + package test.auth.config + max_sessions = 5 + - | + package test + main := result if { + perms := data.test.auth.policies.admin_policy.permissions + max_sess := data.test.auth.config.max_sessions + result := {"permissions": perms, "max_sessions": max_sess} + } + query: data.test.main + want_result: {"permissions": ["read", "write", "delete"], "max_sessions": 5} + + - note: undefined_chain_access + data: {} + modules: + - | + package test + main := result if { + result := data.nonexistent.path.value + } + query: data.test.main + want_result: "#undefined" + + - note: variable_in_nested_scope + data: {} + modules: + - | + package test.utils + default_config = {"retries": 3, "timeout": 30} + - | + package test + main := result if { + outer_var := "outer" + some x in [1, 2] + inner_var := "inner" + config := data.test.utils.default_config + result := { + "outer": outer_var, + "inner": inner_var, + "x": x, + "retries": config.retries + } + x == 2 + } + query: data.test.main + want_result: {"outer": "outer", "inner": "inner", "x": 2, "retries": 3} + + - note: computed_field_name_access + data: {} + modules: + - | + package test.api + endpoints = { + "v1_users": "/api/v1/users", + "v1_posts": "/api/v1/posts", + "v2_users": "/api/v2/users" + } + - | + package test + main := result if { + version := "v1" + resource := "users" + key := sprintf("%s_%s", [version, resource]) + result := data.test.api.endpoints[key] + } + query: data.test.main + want_result: "/api/v1/users" diff --git a/tests/rvm/rego/cases/comparisons.yaml b/tests/rvm/rego/cases/comparisons.yaml new file mode 100644 index 0000000..99b6e92 --- /dev/null +++ b/tests/rvm/rego/cases/comparisons.yaml @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Comparison Operations Test Suite +# Tests comparison operators: ==, <, >, <=, >=, != + +cases: + - note: comparison_equals + data: {} + modules: + - | + package test + main := result if { + result := (5 == 5) + } + query: data.test.main + want_result: true + + - note: comparison_not_equals + data: {} + modules: + - | + package test + main := result if { + result := (5 == 3) + } + query: data.test.main + want_result: false + + - note: comparison_less_than + data: {} + modules: + - | + package test + main := result if { + result := (3 < 5) + } + query: data.test.main + want_result: true + + - note: comparison_greater_than + data: {} + modules: + - | + package test + main := result if { + result := (7 > 5) + } + query: data.test.main + want_result: true diff --git a/tests/rvm/rego/cases/comprehensions.yaml b/tests/rvm/rego/cases/comprehensions.yaml new file mode 100644 index 0000000..a3851c1 --- /dev/null +++ b/tests/rvm/rego/cases/comprehensions.yaml @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Arithmetic Operations Test Suite +# Tests basic arithmetic operations: addition, multiplication, division, subtraction + +cases: + - note: comprehension_basic + data: {} + modules: + - | + package test + main := [(x * 2) | some x in [1, 2, 3]] + query: data.test.main + want_result: [2, 4, 6] diff --git a/tests/rvm/rego/cases/default_rules.yaml b/tests/rvm/rego/cases/default_rules.yaml new file mode 100644 index 0000000..d7a785c --- /dev/null +++ b/tests/rvm/rego/cases/default_rules.yaml @@ -0,0 +1,232 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Default Rules Test Suite +# Tests default rule evaluation when complete rules have no successful definitions + +cases: + - note: default_rule_basic + data: {} + modules: + - | + package test + default allow := false + allow := true if { + false # This will always fail + } + query: data.test.allow + want_result: false + + - note: default_rule_with_multiple_definitions_all_fail + data: {} + modules: + - | + package test + default result := "default_value" + result := "success1" if { + false # This will fail + } + result := "success2" if { + input.nonexistent == "value" # This will fail + } + result := "success3" if { + 1 == 2 # This will fail + } + query: data.test.result + want_result: "default_value" + + - note: default_rule_not_used_when_definition_succeeds + data: {} + modules: + - | + package test + default allow := false + allow := true if { + 1 == 1 # This will succeed + } + query: data.test.allow + want_result: true + + - note: default_rule_with_object_key + skip: true # TODO: Fix rule type classification for config["timeout"] - should be Complete, not PartialObject + data: {} + modules: + - | + package test + default config["timeout"] := 30 + config["timeout"] := 60 if { + false # This will fail + } + query: data.test.config.timeout + want_result: 30 + + - note: default_rule_complex_value + data: {} + modules: + - | + package test + default settings := { + "enabled": false, + "retries": 3, + "timeout": 30 + } + settings := { + "enabled": true, + "retries": 5, + "timeout": 60 + } if { + false # This will fail + } + query: data.test.settings + want_result: + enabled: false + retries: 3 + timeout: 30 + + - note: default_rule_with_array + data: {} + modules: + - | + package test + default items := ["default1", "default2"] + items := ["actual1", "actual2"] if { + false # This will fail + } + query: data.test.items + want_result: ["default1", "default2"] + + - note: default_rule_with_input_dependency + data: {} + modules: + - | + package test + default result := "no_user" + result := "admin" if { + input.user.role == "admin" + } + result := "user" if { + input.user.role == "user" + } + query: data.test.result + want_result: "no_user" + + - note: default_rule_with_input_dependency_success + data: {} + input: + user: + role: "admin" + modules: + - | + package test + default result := "no_user" + result := "admin" if { + input.user.role == "admin" + } + result := "user" if { + input.user.role == "user" + } + query: data.test.result + want_result: "admin" + + - note: default_rule_with_data_dependency + data: + config: + mode: "production" + modules: + - | + package test + default debug_mode := false + debug_mode := true if { + data.config.mode == "development" + } + query: data.test.debug_mode + want_result: false + + - note: default_rule_nested_package + data: {} + modules: + - | + package test.auth + default allow := false + allow := true if { + false # This will fail + } + query: data.test.auth.allow + want_result: false + + - note: multiple_default_rules_different_names + skip: true + data: {} + modules: + - | + package test + default allow := false + default deny := true + allow := true if { + false # This will fail + } + deny := false if { + false # This will fail + } + query: data.test + want_result: + allow: false + deny: true + + - note: default_rule_with_computed_value + skip: true + data: {} + modules: + - | + package test + base_timeout := 10 + default timeout := base_timeout * 3 + timeout := base_timeout * 6 if { + false # This will fail + } + query: data.test.timeout + want_result: 30 + + - note: default_rule_undefined_vs_default + skip: true + data: {} + modules: + - | + package test + default has_default := "default" + # no_default rule has no default and no successful definitions + no_default := "success" if { + false # This will fail + } + query: data.test + want_result: + has_default: "default" + + - note: default_rule_with_function_call + skip: true + data: {} + modules: + - | + package test + helper_func := "helper_result" + default result := helper_func + result := "success" if { + false # This will fail + } + query: data.test.result + want_result: "helper_result" + + - note: default_rule_consistency_check + data: {} + modules: + - | + package test + default value := 42 + value := 42 if { + true # This succeeds with same value as default + } + value := 99 if { + false # This fails + } + query: data.test.value + want_result: 42 diff --git a/tests/rvm/rego/cases/destructuring.yaml b/tests/rvm/rego/cases/destructuring.yaml new file mode 100644 index 0000000..bd3f328 --- /dev/null +++ b/tests/rvm/rego/cases/destructuring.yaml @@ -0,0 +1,260 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Destructuring Pattern Test Suite +# Tests destructuring patterns in assignments, function parameters, and some-in loops +# Note: Set destructuring is not supported by Rego and should produce compilation errors + +cases: + # Basic array destructuring with colon assignment + - note: array_destructuring_basic + data: {} + modules: + - | + package test + main := [a, b] if { + [a, b] := [1, 2] + } + query: data.test.main + want_result: [1, 2] + + # Array destructuring with equals assignment + - note: array_destructuring_equals + data: {} + modules: + - | + package test + main := [x, y, z] if { + arr := [10, 20, 30] + [x, y, z] = arr + } + query: data.test.main + want_result: [10, 20, 30] + + # Object destructuring with colon assignment + - note: object_destructuring_basic + data: {} + modules: + - | + package test + main := [name, age] if { + {"name": name, "age": age} := {"name": "Alice", "age": 30} + } + query: data.test.main + want_result: ["Alice", 30] + + # Object destructuring with equals assignment + - note: object_destructuring_equals + data: {} + modules: + - | + package test + main := [x, y] if { + obj := {"x": 100, "y": 200} + {"x": x, "y": y} = obj + } + query: data.test.main + want_result: [100, 200] + + # Nested array destructuring + - note: nested_array_destructuring + data: {} + modules: + - | + package test + main := [a, c, d] if { + [[a, b], [c, d]] := [[1, 2], [3, 4]] + } + query: data.test.main + want_result: [1, 3, 4] + + # Array destructuring in function parameters + - note: array_destructuring_function_param + data: {} + modules: + - | + package test + add_first_two([x, y]) := x + y + main := add_first_two([5, 7]) + query: data.test.main + want_result: 12 + + # Object destructuring in function parameters + - note: object_destructuring_function_param + data: {} + modules: + - | + package test + get_name({"name": name}) := name + main := get_name({"name": "Bob", "age": 25}) + query: data.test.main + want_result: "Bob" + + # Mixed array and object destructuring + - note: mixed_destructuring + data: {} + modules: + - | + package test + main := [name, x, y] if { + [user, {"x": x, "y": y}] := [{"name": "Grace"}, {"x": 1, "y": 2}] + {"name": name} = user + } + query: data.test.main + want_result: ["Grace", 1, 2] + + # Destructuring with literal matching + - note: destructuring_with_literals + data: {} + modules: + - | + package test + main := value if { + [1, value, 3] := [1, 42, 3] + } + query: data.test.main + want_result: 42 + + # SET DESTRUCTURING ERROR CASES - These should fail compilation + # RVM correctly rejects these, but interpreter incorrectly allows them + + # Set destructuring in colon assignment should error + - note: set_destructuring_colon_error + data: {} + modules: + - | + package test + main := result if { + {a, b} := {1, 2, 3} + result := [a, b] + } + query: data.test.main + want_error: "assignment operator := requires left-hand side to have bindable variables" + allow_interpreter_success: true + + # Set destructuring in function parameters should error + - note: set_destructuring_function_param_error + data: {} + modules: + - | + package test + has_element({x, y}, elem) := elem in {x, y} + main := has_element({10, 20}, 20) + query: data.test.main + want_error: "Undefined variable" + allow_interpreter_success: true + + # Set destructuring in equals assignment should error + - note: set_destructuring_equals_error + data: {} + modules: + - | + package test + main := result if { + s := {1, 2} + {x, y} = s + result := [x, y] + } + query: data.test.main + want_error: "Undefined variable" + allow_interpreter_success: true + + # Option 2: Function parameter destructuring with multiple definitions and definition-level failure + - note: function_param_destructuring_multiple_definitions + data: {} + modules: + - | + package test + # This function has multiple definitions with different parameter patterns + # Only the matching definition should succeed, others should fail at definition level + process_input([x, y]) := sprintf("array: %v, %v", [x, y]) + process_input([x, y, z]) := sprintf("array: %v, %v, %v", [x, y, z]) + process_input({"name": name, "age": age}) := sprintf("object: %s is %d", [name, age]) + + # Test with 2-element array - should match first definition + test_2_elements := process_input([1, 2]) + + # Test with 3-element array - should match second definition + test_3_elements := process_input([1, 2, 3]) + + # Test with object - should match third definition + test_object := process_input({"name": "Alice", "age": 30}) + + # Combined result for testing + main := { + "test_2_elements": test_2_elements, + "test_3_elements": test_3_elements, + "test_object": test_object + } + query: data.test.main + want_result: + test_2_elements: "array: 1, 2" + test_3_elements: "array: 1, 2, 3" + test_object: "object: Alice is 30" + + + # Option 2: Complex nested destructuring in function parameters + - note: function_param_nested_destructuring + data: {} + modules: + - | + package test + # Function with nested destructuring patterns + extract_info({"user": {"name": name, "details": {"age": age, "city": city}}, "active": active}) := { + "user_name": name, + "user_age": age, + "user_city": city, + "is_active": active + } + + main := extract_info({ + "user": { + "name": "Bob", + "details": { + "age": 25, + "city": "Seattle" + } + }, + "active": true + }) + query: data.test.main + want_result: + user_name: "Bob" + user_age: 25 + user_city: "Seattle" + is_active: true + + # Option 2: Mixed destructuring and non-destructuring definitions + - note: function_mixed_destructuring_and_simple + data: {} + modules: + - | + package test + # Function with mixed parameter styles - some with destructuring, some without + handle_request(method) := sprintf("simple method: %s", [method]) if { + method in ["GET", "POST", "PUT", "DELETE"] + } + handle_request({"method": method, "path": path}) := sprintf("structured request: %s %s", [method, path]) + handle_request({"method": method, "headers": {"auth": token}}) := sprintf("authenticated %s with token %s", [method, token]) + + # Test simple string parameter - should match first definition + test_simple := handle_request("GET") + + # Test structured request - should match second definition + test_structured := handle_request({"method": "POST", "path": "/users"}) + + # Test with auth header - should match third definition + test_auth := handle_request({"method": "PUT", "headers": {"auth": "abc123"}}) + + # Test that fails all patterns - this should be undefined + test_invalid := handle_request(42) + + # Combined result for testing + main := { + "test_simple": test_simple, + "test_structured": test_structured, + "test_auth": test_auth, + "test_invalid": test_invalid + } + query: data.test.main + want_result: "#undefined" \ No newline at end of file diff --git a/tests/rvm/rego/cases/examples.yaml b/tests/rvm/rego/cases/examples.yaml new file mode 100644 index 0000000..88d6b62 --- /dev/null +++ b/tests/rvm/rego/cases/examples.yaml @@ -0,0 +1,72 @@ +cases: + - note: server_security_policy + data: {} + input: + servers: + - id: "app" + protocols: ["https", "ssh"] + ports: ["p1", "p2", "p3"] + - id: "db" + protocols: ["mysql"] + ports: ["p3"] + - id: "cache" + protocols: ["memcache"] + ports: ["p3"] + - id: "ci" + protocols: ["http"] + ports: ["p1", "p2"] + - id: "busybox" + protocols: ["telnet"] + ports: ["p1"] + networks: + - id: "net1" + public: false + - id: "net2" + public: false + - id: "net3" + public: true + - id: "net4" + public: true + ports: + - id: "p1" + network: "net1" + - id: "p2" + network: "net3" + - id: "p3" + network: "net2" + modules: + - | + package example + + default allow := false # unless otherwise defined, allow is false + + allow := r if { # allow is true if... + r := { + "outcome": count(violation) == 0, # there are zero violations. + "violations": violation # the violations are listed in the output. + } + } + + violation contains server.id if { # a server is in the violation set if... + server := input.servers[_] # it exists in the input.servers collection and... + server.protocols[_] == "telnet" # it contains the "telnet" protocol. + } + + violation contains server.id if { # a server is in the violation set if... + some server + public_server[server] # it exists in the 'public_server' set and... + server.protocols[_] == "http" # it contains the insecure "http" protocol. + } + + public_server contains server if { # a server exists in the public_server set if... + some i, j + server := input.servers[_] # it exists in the input.servers collection and... + server.ports[_] == input.ports[i].id # it references a port in the input.ports collection and... + input.ports[i].network == input.networks[j].id # the port references a network in the input.networks collection and... + input.networks[j].public # the network is public. + } + query: data.example.allow + want_result: + outcome: false + violations: + set!: ["ci", "busybox"] diff --git a/tests/rvm/rego/cases/function_rules.yaml b/tests/rvm/rego/cases/function_rules.yaml new file mode 100644 index 0000000..a49c7ab --- /dev/null +++ b/tests/rvm/rego/cases/function_rules.yaml @@ -0,0 +1,230 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Function Rules Test Suite +# Tests user-defined function rule calls with arguments +# Covers function definitions, argument passing, return values, and consistency + +cases: + - note: simple_function_call + description: Test basic function rule definition and call + data: {} + modules: + - | + package test + + # Define a simple function rule + add_ten(x) := x + 10 + + # Call the function + main := add_ten(5) + query: data.test.main + want_result: 15 + + - note: function_with_multiple_args + description: Test function rule with multiple arguments + data: {} + modules: + - | + package test + + # Function that adds two numbers + add(x, y) := x + y + + # Call with two arguments + main := add(7, 3) + query: data.test.main + want_result: 10 + + - note: function_with_variable_args + description: Test function call with variables as arguments + data: {} + modules: + - | + package test + + multiply(x, y) := x * y + + main := result if { + a := 4 + b := 6 + result := multiply(a, b) + } + query: data.test.main + want_result: 24 + + - note: function_returning_object + description: Test function that returns an object + data: {} + modules: + - | + package test + + make_person(name, age) := {"name": name, "age": age} + + main := make_person("Alice", 30) + query: data.test.main + want_result: {"name": "Alice", "age": 30} + + - note: function_returning_array + description: Test function that returns an array + data: {} + modules: + - | + package test + + make_range(start, end) := [start, end] if start <= end + + main := make_range(1, 3) + query: data.test.main + want_result: [1, 3] + + - note: nested_function_calls + description: Test nested function calls + data: {} + modules: + - | + package test + + double(x) := x * 2 + add_one(x) := x + 1 + + main := double(add_one(5)) + query: data.test.main + want_result: 12 + + - note: function_with_condition + description: Test function rule with conditional body + data: {} + modules: + - | + package test + + max(x, y) := x if x >= y + max(x, y) := y if y > x + + main := max(7, 3) + query: data.test.main + want_result: 7 + + - note: function_consistency_check + description: Test that function definitions must be consistent + data: {} + modules: + - | + package test + + # These definitions would be inconsistent if both conditions were true + inconsistent_func(x) := x + 1 if x < 5 + inconsistent_func(x) := x + 2 if x < 5 + + # This should work for x >= 5 + main := inconsistent_func(10) + query: data.test.main + want_result: "#undefined" + + - note: function_with_undefined_result + description: Test function that can return undefined + data: {} + modules: + - | + package test + + # Function only defined for positive numbers + positive_double(x) := x * 2 if x > 0 + + # Calling with negative number should return undefined + main := positive_double(-1) + query: data.test.main + want_result: "#undefined" + + - note: function_using_data + description: Test function that accesses global data + data: {"multiplier": 3} + modules: + - | + package test + + scale(x) := x * data.multiplier + + main := scale(5) + query: data.test.main + want_result: 15 + + - note: function_using_input + description: Test function that accesses input + data: {} + input: {"base": 10} + modules: + - | + package test + + add_to_base(x) := x + input.base + + main := add_to_base(5) + query: data.test.main + want_result: 15 + + - note: function_with_complex_logic + description: Test function with complex conditional logic + data: {} + modules: + - | + package test + + classify_number(x) := "negative" if x < 0 + classify_number(x) := "zero" if x == 0 + classify_number(x) := "small positive" if { + x > 0 + x <= 10 + } + classify_number(x) := "large positive" if x > 10 + + main := classify_number(5) + query: data.test.main + want_result: "small positive" + + - note: function_with_array_processing + description: Test function that processes arrays + data: {} + modules: + - | + package test + + first_element(arr) := arr[0] + + main := first_element([1, 2, 3]) + query: data.test.main + want_result: 1 + + - note: function_with_object_processing + description: Test function that processes objects + data: {} + modules: + - | + package test + + get_field(obj, field) := obj[field] + + main := get_field({"name": "Bob", "age": 25}, "name") + query: data.test.main + want_result: "Bob" + + - note: function_call_chain + description: Test chain of function calls + data: {} + modules: + - | + package test + + step1(x) := x + 1 + step2(x) := x * 2 + step3(x) := x - 3 + + main := result if { + a := step1(5) # 6 + b := step2(a) # 12 + result := step3(b) # 9 + } + query: data.test.main + want_result: 9 diff --git a/tests/rvm/rego/cases/local_chained_access.yaml b/tests/rvm/rego/cases/local_chained_access.yaml new file mode 100644 index 0000000..dec6eda --- /dev/null +++ b/tests/rvm/rego/cases/local_chained_access.yaml @@ -0,0 +1,322 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Non-Data Prefix Chained Access Test Suite +# Tests chained reference expressions without data prefix (local rules and variables) + +cases: + - note: direct_rule_field_access + data: {} + modules: + - | + package test + user_config = {"name": "Alice", "role": "admin", "active": true} + main := result if { + result := user_config.name + } + query: data.test.main + want_result: "Alice" + + - note: chained_rule_field_access + data: {} + modules: + - | + package test + settings = { + "database": {"host": "localhost", "port": 5432}, + "cache": {"enabled": true, "ttl": 300} + } + main := result if { + result := settings.database.host + } + query: data.test.main + want_result: "localhost" + + - note: local_variable_with_fields + data: {} + modules: + - | + package test + main := result if { + config := {"server": {"name": "web1", "port": 8080}} + result := config.server.name + } + query: data.test.main + want_result: "web1" + + - note: rule_access_with_dynamic_index + data: {} + modules: + - | + package test + servers = { + "web": {"status": "running", "cpu": 45}, + "db": {"status": "stopped", "cpu": 0} + } + main := result if { + server_type := "web" + result := servers[server_type].status + } + query: data.test.main + want_result: "running" + + - note: mixed_static_dynamic_local_access + data: {} + modules: + - | + package test + metrics = { + "hourly": [ + {"timestamp": "2023-01-01T10:00:00Z", "value": 100}, + {"timestamp": "2023-01-01T11:00:00Z", "value": 150} + ] + } + main := result if { + period := "hourly" + index := 1 + result := metrics[period][index].value + } + query: data.test.main + want_result: 150 + + - note: nested_rule_calls_without_data_prefix + data: {} + modules: + - | + package test + base_config = {"timeout": 30, "retries": 3} + extended_config = {"base": base_config, "debug": true} + main := result if { + result := extended_config.base.timeout + } + query: data.test.main + want_result: 30 + + - note: local_var_precedence_over_same_package_rule + data: {} + modules: + - | + package test + config = {"source": "rule"} + main := result if { + config := {"source": "local"} + result := config.source + } + query: data.test.main + want_result: "local" + + - note: array_access_without_data_prefix + data: {} + modules: + - | + package test + items = [ + {"id": 1, "name": "first"}, + {"id": 2, "name": "second"}, + {"id": 3, "name": "third"} + ] + main := result if { + idx := 2 + result := items[idx].name + } + query: data.test.main + want_result: "third" + + - note: string_literal_bracket_local_access + data: {} + modules: + - | + package test + status_codes = { + "200": "OK", + "404": "Not Found", + "500": "Internal Server Error" + } + main := result if { + result := status_codes["404"] + } + query: data.test.main + want_result: "Not Found" + + - note: complex_local_chaining + data: {} + modules: + - | + package test + app_config = { + "environments": { + "dev": { + "database": {"url": "dev.db.com", "pool_size": 5}, + "logging": {"level": "debug"} + }, + "prod": { + "database": {"url": "prod.db.com", "pool_size": 20}, + "logging": {"level": "error"} + } + } + } + main := result if { + env := "prod" + result := app_config.environments[env].database.url + } + query: data.test.main + want_result: "prod.db.com" + + - note: rule_with_computed_field_access + data: {} + modules: + - | + package test + api_versions = { + "v1": {"path": "/api/v1", "deprecated": true}, + "v2": {"path": "/api/v2", "deprecated": false} + } + main := result if { + version := "v2" + field := "deprecated" + result := api_versions[version][field] + } + query: data.test.main + want_result: false + + - note: nested_local_variables_with_chaining + data: {} + modules: + - | + package test + main := result if { + user := {"profile": {"settings": {"theme": "dark", "notifications": true}}} + theme_setting := user.profile.settings.theme + result := theme_setting + } + query: data.test.main + want_result: "dark" + + - note: rule_reference_with_multiple_field_access + data: {} + modules: + - | + package test + network_config = { + "interfaces": { + "eth0": {"ip": "192.168.1.10", "mask": "255.255.255.0"}, + "eth1": {"ip": "10.0.0.5", "mask": "255.255.0.0"} + } + } + main := result if { + interface := "eth0" + result := { + "ip": network_config.interfaces[interface].ip, + "mask": network_config.interfaces[interface].mask + } + } + query: data.test.main + want_result: {"ip": "192.168.1.10", "mask": "255.255.255.0"} + + - note: undefined_rule_field_access + data: {} + modules: + - | + package test + main := result if { + result := nonexistent_rule.field + } + query: data.test.main + want_error: "undefined variable" + + - note: undefined_field_on_existing_rule + data: {} + modules: + - | + package test + my_rule = {"existing": "value"} + main := result if { + result := my_rule.nonexistent_field + } + query: data.test.main + want_result: "#undefined" + + - note: variable_assignment_with_chained_access + data: {} + modules: + - | + package test + source_data = { + "users": { + "alice": {"email": "alice@example.com", "active": true}, + "bob": {"email": "bob@example.com", "active": false} + } + } + main := result if { + user_id := "alice" + user_email := source_data.users[user_id].email + result := user_email + } + query: data.test.main + want_result: "alice@example.com" + + - note: rule_call_in_middle_of_chain + data: {} + modules: + - | + package test + get_user_data = {"profile": {"name": "Alice", "age": 30}} + main := result if { + result := get_user_data.profile.name + } + query: data.test.main + want_result: "Alice" + + - note: local_var_shadowing_with_different_structure + data: {} + modules: + - | + package test + config = {"type": "global", "value": 100} + main := result if { + config := [{"type": "local", "value": 200}] + result := config[0].type + } + query: data.test.main + want_result: "local" + + - note: deep_nested_field_access + data: {} + modules: + - | + package test + deep_structure = { + "level1": { + "level2": { + "level3": { + "level4": { + "level5": {"final_value": "found it!"} + } + } + } + } + } + main := result if { + result := deep_structure.level1.level2.level3.level4.level5.final_value + } + query: data.test.main + want_result: "found it!" + + - note: bracket_access_with_computed_key + data: {} + modules: + - | + package test + lookup_table = { + "key_1": "value_1", + "key_2": "value_2", + "key_3": "value_3" + } + main := result if { + prefix := "key" + suffix := 2 + key := sprintf("%s_%d", [prefix, suffix]) + result := lookup_table[key] + } + query: data.test.main + want_result: "value_2" diff --git a/tests/rvm/rego/cases/loops_and_quantifiers.yaml b/tests/rvm/rego/cases/loops_and_quantifiers.yaml new file mode 100644 index 0000000..1449e41 --- /dev/null +++ b/tests/rvm/rego/cases/loops_and_quantifiers.yaml @@ -0,0 +1,89 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Loops and Quantifiers Test Suite +# Tests basic loop constructs, quantifiers (some/every), and comprehensions + +cases: + - note: basic_variable_test + data: {} + modules: + - | + package test + main := result if { + x := 5 + result := x > 2 + } + query: data.test.main + want_result: true + + - note: basic_some_test + data: {} + modules: + - | + package test + main := result if { + some x in [1, 2, 3] + x > 2 + result := x + } + query: data.test.main + want_result: 3 + + - note: basic_every_test + data: {} + modules: + - | + package test + main := result if { + every x in [1, 2, 3] { + x > 0 + } + result := true + } + query: data.test.main + want_result: true + + - note: simple_loop_test + data: {} + modules: + - | + package test + main := result if { + x := 1 + y := 2 + result := x * y + } + query: data.test.main + want_result: 2 + + - note: loop_array_comprehension + data: {} + modules: + - | + package test + main := [(x * 2) | x := [1, 2, 3][_]] + query: data.test.main + want_result: [2, 4, 6] + + - note: loop_set_comprehension + data: {} + modules: + - | + package test + main := {(x * 2) | x := [1, 2, 3][_]} + query: data.test.main + want_result: + set!: [2, 4, 6] + + - note: loop_object_comprehension + data: {} + modules: + - | + package test + main := {k: (v * 2) | v := {"a": 1, "b": 2, "c": 3}[k]} + query: data.test.main + want_result: + a: 2 + b: 4 + c: 6 diff --git a/tests/rvm/rego/cases/multiple_entry_points.yaml b/tests/rvm/rego/cases/multiple_entry_points.yaml new file mode 100644 index 0000000..4c642fa --- /dev/null +++ b/tests/rvm/rego/cases/multiple_entry_points.yaml @@ -0,0 +1,78 @@ +cases: + - note: "multiple entry points - basic allow and deny rules" + modules: + - | + package example + + default allow := false + + allow if { + input.method == "GET" + } + + allow if { + input.user == "admin" + } + + default deny := true + + deny if { + input.method == "DELETE" + } + query: "data.example.allow" + entry_points: + - "data.example.allow" + - "data.example.deny" + input: {"method": "GET", "user": "guest"} + want_result: true + + - note: "multiple entry points - computed rules with want_results" + modules: + - | + package math + + result := 42 + + doubled := 84 + + status := "computed" + query: "data.math.result" + entry_points: + - "data.math.result" + - "data.math.doubled" + - "data.math.status" + want_results: + - 42 + - 84 + - "computed" + + - note: "multiple entry points - different packages with want_results" + modules: + - | + package auth + + default authenticated := false + + authenticated if { + input.token == "valid" + } + - | + package authz + + default authorized := false + + authorized if { + input.user == "admin" + } + + authorized if { + input.role == "manager" + } + query: "data.auth.authenticated" + entry_points: + - "data.auth.authenticated" + - "data.authz.authorized" + input: {"token": "valid", "user": "guest"} + want_results: + - true + - false \ No newline at end of file diff --git a/tests/rvm/rego/cases/objects.yaml b/tests/rvm/rego/cases/objects.yaml new file mode 100644 index 0000000..343c179 --- /dev/null +++ b/tests/rvm/rego/cases/objects.yaml @@ -0,0 +1,108 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Objects Test Suite +# Tests object creation, nested objects, field access, and dynamic field operations + +cases: + - note: object_creation + data: {} + modules: + - | + package test + main := result if { + result := {"name": "Alice", "age": 30, "active": true} + } + query: data.test.main + want_result: {"name": "Alice", "age": 30, "active": true} + + - note: nested_objects + data: {} + modules: + - | + package test + main := result if { + result := { + "user": { + "name": "Alice", + "details": {"age": 30, "active": true} + }, + "config": {"debug": false} + } + } + query: data.test.main + want_result: {"user": {"name": "Alice", "details": {"age": 30, "active": true}}, "config": {"debug": false}} + + - note: object_field_access + data: {} + modules: + - | + package test + user := {"name": "Alice", "age": 30} + main := result if { + result := user.name + } + query: data.test.main + want_result: "Alice" + + - note: nested_object_access + data: {} + modules: + - | + package test + user := { + "name": "Alice", + "details": { + "age": 30, + "profile": { + "country": "USA", + "city": "Seattle" + } + } + } + main := result if { + result := user.details.profile.city + } + query: data.test.main + want_result: "Seattle" + + - note: dynamic_field_name_get + data: {} + modules: + - | + package test + user := {"name": "Alice", "age": 30, "status": "active"} + field_name := "status" + main := result if { + result := user[field_name] + } + query: data.test.main + want_result: "active" + + - note: dynamic_field_name_set + data: {} + modules: + - | + package test + field_name := "email" + field_value := "alice@example.com" + main := result if { + result := {field_name: field_value, "name": "Alice"} + } + query: data.test.main + want_result: {"email": "alice@example.com", "name": "Alice"} + + - note: dynamic_object_construction + data: {} + modules: + - | + package test + name_field := "username" + name_value := "alice123" + age_field := "user_age" + age_value := 25 + main := result if { + result := {name_field: name_value, age_field: age_value} + } + query: data.test.main + want_result: {"username": "alice123", "user_age": 25} diff --git a/tests/rvm/rego/cases/rule_data_conflicts.yaml b/tests/rvm/rego/cases/rule_data_conflicts.yaml new file mode 100644 index 0000000..0b270a5 --- /dev/null +++ b/tests/rvm/rego/cases/rule_data_conflicts.yaml @@ -0,0 +1,145 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Rule-Data Conflict Detection Test Suite +# Tests that the RVM properly detects conflicts between rule definitions and data documents + +cases: + - note: no_conflict_different_packages + data: + users: + alice: + role: "guest" + level: 1 + modules: + - | + package test + result := data.users.alice.role + query: data.test.result + want_result: "guest" + + - note: no_conflict_different_paths + data: + config: + database: + host: "localhost" + port: 5432 + modules: + - | + package test + users := { + "alice": { + "role": "admin" + } + } + query: data.test.users + want_result: + alice: + role: "admin" + + - note: conflict_same_path_rule_vs_data + data: + test: + users: + alice: + role: "guest" + modules: + - | + package test.users + alice := { + "role": "admin", + "level": 5 + } + query: data.test.users.alice + want_error: "Conflict: rule defines path 'test.users.alice' but data also provides this path" + # RVM detects this conflict, but interpreter may not - that's acceptable + allow_interpreter_success: true + + - note: conflict_rule_parent_data_child + data: + test: + config: + database: + host: "localhost" + port: 5432 + modules: + - | + package test + config := { + "app_name": "myapp", + "version": "1.0" + } + query: data.test.config + want_error: "Conflict: rule defines path 'test.config' but data also provides this path" + # RVM detects this conflict, but interpreter may not - that's acceptable + allow_interpreter_success: true + + - note: conflict_data_parent_rule_child + data: + test: + users: "not an object" + modules: + - | + package test.users + alice := {"role": "admin"} + query: data.test.users.alice + want_error: "Conflict: rule defines subpaths under 'test.users' but data provides a non-object value at this path" + # RVM detects this conflict, but interpreter may not - that's acceptable + allow_interpreter_success: true + + - note: no_conflict_nested_coexistence + data: + static_config: + database: + host: "localhost" + port: 5432 + user_data: + preferences: + theme: "dark" + modules: + - | + package dynamic + users := { + "alice": { + "role": "admin" + } + } + computed_stats := { + "total_users": 42 + } + query: data.dynamic.users + want_result: + alice: + role: "admin" + + - note: no_conflict_multiple_rule_levels + data: + test: + api: + v1: + endpoints: ["users", "posts"] + modules: + - | + package test.api.v1 + auth := { + "required": true, + "methods": ["jwt", "oauth"] + } + query: data.test.api.v1.auth + want_result: + required: true + methods: ["jwt", "oauth"] + + - note: no_conflict_rule_extends_data_object + data: + test: + config: + database: + host: "localhost" + modules: + - | + package test.config + app_name := "myapp" + version := "1.0" + query: data.test.config.app_name + want_result: "myapp" diff --git a/tests/rvm/rego/cases/set_rules.yaml b/tests/rvm/rego/cases/set_rules.yaml new file mode 100644 index 0000000..f74b935 --- /dev/null +++ b/tests/rvm/rego/cases/set_rules.yaml @@ -0,0 +1,153 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Examples Test Suite +# Tests real-world patterns and advanced Rego constructs + +cases: + - note: set_rules_with_contains + data: {} + input: + user: + role: "editor" + name: "alice" + modules: + - | + package test + + # Define a set of allowed actions + allowed_actions contains "read" if { + input.user.role in ["viewer", "editor", "admin"] + } + + allowed_actions contains "write" if { + input.user.role in ["editor", "admin"] + } + + allowed_actions contains "admin" if { + input.user.role == "admin" + } + + # Check if a specific action is allowed + allow_read := "read" in allowed_actions + allow_write := "write" in allowed_actions + allow_admin := "admin" in allowed_actions + + # Main result combining all permissions + main := { + "allowed_actions": allowed_actions, + "can_read": allow_read, + "can_write": allow_write, + "can_admin": allow_admin + } + query: data.test.main + want_result: + allowed_actions: + set!: ["read", "write"] + can_read: true + can_write: true + can_admin: false + + - note: set_membership_with_contains + data: {} + input: + department: "engineering" + role: "developer" + modules: + - | + package test + + # Define sets using contains + valid_departments contains d if { + some dept in ["engineering", "marketing", "sales"] + d := dept + } + + sensitive_roles contains role if { + some role in ["admin", "security", "finance"] + r := role + } + + # Check membership + is_valid_dept := input.department in valid_departments + is_sensitive := input.role in sensitive_roles + + # Access decision + allow := is_valid_dept + deny := is_sensitive + + main := { + "valid_departments": valid_departments, + "sensitive_roles": sensitive_roles, + "department_valid": is_valid_dept, + "role_sensitive": is_sensitive, + "allow": allow, + "deny": deny + } + query: data.test.main + want_result: + valid_departments: + set!: ["engineering", "marketing", "sales"] + sensitive_roles: + set!: ["admin", "security", "finance"] + department_valid: true + role_sensitive: false + allow: true + deny: false + + - note: conditional_set_contains + data: {} + input: + user: + active: true + level: 3 + department: "engineering" + modules: + - | + package test + + # Conditional set rules + permissions contains "read" if { + input.user.active == true + } + + permissions contains "write" if { + input.user.active == true + input.user.level >= 2 + } + + permissions contains "delete" if { + input.user.active == true + input.user.level >= 5 + input.user.department == "admin" + } + + main := permissions + query: data.test.main + want_result: + set!: ["read", "write"] + + - note: empty_set_contains + data: {} + input: + user: + role: "user" + verified: false + modules: + - | + package test + + # Set that might be empty based on conditions + special_permissions contains "super_admin" if { + input.user.role == "root" + input.user.verified == true + } + + special_permissions contains "audit" if { + input.user.role == "auditor" + } + + main := special_permissions + query: data.test.main + want_result: + set!: [] diff --git a/tests/rvm/rego/cases/sets.yaml b/tests/rvm/rego/cases/sets.yaml new file mode 100644 index 0000000..f163683 --- /dev/null +++ b/tests/rvm/rego/cases/sets.yaml @@ -0,0 +1,69 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Sets Test Suite +# Tests set creation, deduplication, membership testing, and nested sets + +cases: + - note: set_creation + data: {} + modules: + - | + package test + main := result if { + result := {1, 2, 3, "hello", true} + } + query: data.test.main + want_result: + set!: [1, 2, 3, "hello", true] + + - note: set_with_duplicates + data: {} + modules: + - | + package test + main := result if { + result := {1, 2, 2, 3, 1} + } + query: data.test.main + want_result: + set!: [1, 2, 3] + + - note: set_membership + data: {} + modules: + - | + package test + my_set := {1, 2, 3, 4, 5} + main := result if { + result := 3 in my_set + } + query: data.test.main + want_result: true + + - note: set_non_membership + data: {} + modules: + - | + package test + my_set := {"a", "b", "c"} + main := result if { + result := "d" in my_set + } + query: data.test.main + want_result: false + + - note: nested_sets + data: {} + modules: + - | + package test + main := result if { + result := {{1, 2}, {3, 4}, {"a", "b"}} + } + query: data.test.main + want_result: + set!: + - set!: [1, 2] + - set!: [3, 4] + - set!: ["a", "b"] diff --git a/tests/rvm/rego/cases/variables_and_rules.yaml b/tests/rvm/rego/cases/variables_and_rules.yaml new file mode 100644 index 0000000..681ed21 --- /dev/null +++ b/tests/rvm/rego/cases/variables_and_rules.yaml @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Variables and Rules Test Suite +# Tests variable assignment, rule definitions, and rule dependencies + +cases: + - note: variable_assignment + data: {} + modules: + - | + package test + main := result if { + x := 42 + result := x + } + query: data.test.main + want_result: 42 + + - note: rule_without_body + data: {} + modules: + - | + package test + main = 100 + query: data.test.main + want_result: 100 + + - note: rule_dependency + data: {} + modules: + - | + package test + x = 42 + main := result if { + result := x + 10 + } + query: data.test.main + want_result: 52 + + - note: rule_undefined_condition_fails + data: {} + modules: + - | + package test + main := "success" if { + false # condition always fails + } + query: data.test.main + want_result: "#undefined" diff --git a/tests/rvm/rego/cases/virtual_data_document_lookup.yaml b/tests/rvm/rego/cases/virtual_data_document_lookup.yaml new file mode 100644 index 0000000..952c68d --- /dev/null +++ b/tests/rvm/rego/cases/virtual_data_document_lookup.yaml @@ -0,0 +1,396 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# VirtualDataDocumentLookup Test Suite +# Tests the four cases of virtual data document lookup: +# 1. All components consumed and rule index found -> evaluate rule +# 2. Rule index found with remaining components -> evaluate rule then index result +# 3. All components consumed but undefined -> apply components to data directly +# 4. Subobject found -> panic (not yet implemented) + +cases: + # Case 1: All components consumed and rule index found + - note: rule_index_all_components_consumed + data: + users: + alice: {"name": "Alice", "age": 30} + input: + rule_name: "alice_profile" + modules: + - | + package test.users + alice_profile := data.users.alice + - | + package test + main := result if { + result := data.test.users[input.rule_name] + } + query: data.test.main + want_result: {"name": "Alice", "age": 30} + allow_interpreter_incorrect_behavior: true + + # Case 2: Rule index found with remaining components + - note: rule_index_with_remaining_components + data: + users: + alice: {"name": "Alice", "age": 30, "profile": {"bio": "Software Engineer"}} + input: + rule_name: "alice_data" + field1: "profile" + field2: "bio" + modules: + - | + package test.users + alice_data := data.users.alice + - | + package test + main := result if { + result := data.test.users[input.rule_name][input.field1][input.field2] + } + query: data.test.main + want_result: "Software Engineer" + allow_interpreter_incorrect_behavior: true + + # Case 2b: Rule index with multiple remaining components + - note: rule_index_multiple_remaining_components + data: + config: + app: + settings: {"theme": "dark", "lang": "en"} + input: + rule: "app_config" + path1: "settings" + path2: "theme" + modules: + - | + package test.config + app_config := data.config.app + - | + package test + main := result if { + result := data.test.config[input.rule][input.path1][input.path2] + } + query: data.test.main + want_result: "dark" + allow_interpreter_incorrect_behavior: true + + # Case 3: All components consumed but no rule exists (direct data access) + - note: direct_data_access_no_rules + data: + users: + bob: {"name": "Bob", "age": 25} + input: + person: "bob" + attribute: "name" + modules: + - | + package test + main := result if { + # No rule defined for data.users.bob, should access data directly + result := data.users[input.person][input.attribute] + } + query: data.test.main + want_result: "Bob" + + # Case 3b: Direct nested data access (no rules) + - note: direct_nested_data_access + data: + system: + metrics: + cpu: 85 + memory: 70 + input: + metric: "cpu" + modules: + - | + package test + main := result if { + # No rules defined for data.system.metrics, access data directly + result := data.system.metrics[input.metric] + } + query: data.test.main + want_result: 85 + + # Case 3c: Direct array indexing (no rules) + - note: direct_array_indexing + data: + inventory: + fruits: ["apple", "banana", "cherry"] + input: + collection: "fruits" + index: 1 + modules: + - | + package test + main := result if { + # No rules defined for data.inventory.fruits, access array directly + result := data.inventory[input.collection][input.index] + } + query: data.test.main + want_result: "banana" + + # Mixed case: Rule exists at intermediate level, then data access + - note: rule_at_intermediate_level + data: + company: + employees: + - {"name": "Alice", "dept": "Engineering"} + - {"name": "Bob", "dept": "Marketing"} + input: + rule_name: "staff" + idx: 0 + field: "name" + modules: + - | + package test.company + staff := data.company.employees + - | + package test + main := result if { + # Rule exists at data.test.company.staff, then access array element + result := data.test.company[input.rule_name][input.idx][input.field] + } + query: data.test.main + want_result: "Alice" + allow_interpreter_incorrect_behavior: true + + # Case with dynamic indexing via register + - note: rule_with_dynamic_indexing + data: + products: + electronics: {"laptop": 1200, "phone": 800} + clothing: {"shirt": 25, "pants": 50} + input: + rule: "electronics_catalog" + item: "laptop" + modules: + - | + package test.products + electronics_catalog := data.products.electronics + - | + package test + main := result if { + result := data.test.products[input.rule][input.item] + } + query: data.test.main + want_result: 1200 + allow_interpreter_incorrect_behavior: true + + # Case 3d: Undefined path access returns undefined + - note: undefined_path_access + data: {} + input: + path2: "path" + path3: "value" + modules: + - | + package test + main := result if { + # This should access undefined data, returning undefined + result := data.nonexistent[input.path2][input.path3] + } + query: data.test.main + want_result: "#undefined" + + # Case that actually triggers VirtualDataDocumentLookup: + # Path is a prefix of multiple rules + - note: virtual_lookup_with_rule_prefix + data: + config: + app: {"name": "MyApp", "version": "1.0"} + input: + submodule: "app" + field: "name" + modules: + - | + package test.config.app + name := data.config.app.name + version := data.config.app.version + full_info := {"name": data.config.app.name, "version": data.config.app.version} + - | + package test + main := result if { + # This should trigger VirtualDataDocumentLookup since data.test.config.app + # is a prefix of multiple rules: data.test.config.app.name, data.test.config.app.version, etc. + result := data.test.config[input.submodule][input.field] + } + query: data.test.main + want_result: "MyApp" + + # Case 4: Subobject case - evaluate all rules in a subobject and merge with data + - note: subobject_case_multiple_rules + data: + users: + alice: {"name": "Alice", "age": 30} + bob: {"name": "Bob", "age": 25} + permissions: + alice: {"admin": true} + bob: {"admin": false} + modules: + - | + package test.users.alice + profile := {"name": data.users.alice.name, "age": data.users.alice.age} + is_admin := data.permissions.alice.admin + - | + package test.users.bob + profile := {"name": data.users.bob.name, "age": data.users.bob.age} + is_admin := data.permissions.bob.admin + - | + package test + main := result if { + # This should trigger Case 4: all components consumed and we have a subobject + # data.test.users should contain the evaluated rules from both alice and bob packages + result := data.test.users + } + query: data.test.main + want_result: + alice: + profile: {"name": "Alice", "age": 30} + is_admin: true + bob: + profile: {"name": "Bob", "age": 25} + is_admin: false + + # Case 4b: Nested subobject evaluation with cache hits + - note: nested_subobject_with_cache_hits + data: + company: + departments: + engineering: {"budget": 1000000} + marketing: {"budget": 500000} + employees: + alice: {"dept": "engineering", "salary": 100000} + bob: {"dept": "marketing", "salary": 70000} + charlie: {"dept": "engineering", "salary": 90000} + modules: + - | + package test.company.departments.engineering + total_budget := data.company.departments.engineering.budget + employee_count := count([e | e := data.company.employees[_]; e.dept == "engineering"]) + avg_budget_per_employee := total_budget / employee_count + - | + package test.company.departments.marketing + total_budget := data.company.departments.marketing.budget + employee_count := count([e | e := data.company.employees[_]; e.dept == "marketing"]) + avg_budget_per_employee := total_budget / employee_count + - | + package test.company.employees.alice + profile := data.company.employees.alice + department_info := data.test.company.departments[profile.dept] # Should hit cache + - | + package test.company.employees.bob + profile := data.company.employees.bob + department_info := data.test.company.departments[profile.dept] # Should hit cache + - | + package test.company.employees.charlie + profile := data.company.employees.charlie + department_info := data.test.company.departments[profile.dept] # Should hit cache again + - | + package test + main := result if { + # This creates nested subobject evaluations: + # 1. data.test.company (subobject with departments and employees) + # 2. data.test.company.departments (subobject with engineering and marketing) + # 3. data.test.company.employees (subobject with alice, bob, charlie) + # The departments should be cached and reused multiple times + result := { + "company_overview": data.test.company, + "departments_only": data.test.company.departments, # Cache hit for departments + "employees_only": data.test.company.employees, # Cache hit for employees + "engineering_dept": data.test.company.departments.engineering # Cache hit for specific dept + } + } + query: data.test.main + want_result: + company_overview: + departments: + engineering: + total_budget: 1000000 + employee_count: 2 + avg_budget_per_employee: 500000 + marketing: + total_budget: 500000 + employee_count: 1 + avg_budget_per_employee: 500000 + employees: + alice: + profile: {"dept": "engineering", "salary": 100000} + department_info: + total_budget: 1000000 + employee_count: 2 + avg_budget_per_employee: 500000 + bob: + profile: {"dept": "marketing", "salary": 70000} + department_info: + total_budget: 500000 + employee_count: 1 + avg_budget_per_employee: 500000 + charlie: + profile: {"dept": "engineering", "salary": 90000} + department_info: + total_budget: 1000000 + employee_count: 2 + avg_budget_per_employee: 500000 + departments_only: + engineering: + total_budget: 1000000 + employee_count: 2 + avg_budget_per_employee: 500000 + marketing: + total_budget: 500000 + employee_count: 1 + avg_budget_per_employee: 500000 + employees_only: + alice: + profile: {"dept": "engineering", "salary": 100000} + department_info: + total_budget: 1000000 + employee_count: 2 + avg_budget_per_employee: 500000 + bob: + profile: {"dept": "marketing", "salary": 70000} + department_info: + total_budget: 500000 + employee_count: 1 + avg_budget_per_employee: 500000 + charlie: + profile: {"dept": "engineering", "salary": 90000} + department_info: + total_budget: 1000000 + employee_count: 2 + avg_budget_per_employee: 500000 + engineering_dept: + total_budget: 1000000 + employee_count: 2 + avg_budget_per_employee: 500000 + + # Test that function rules are excluded from virtual data document lookup + - note: function_rules_excluded_from_virtual_lookup + data: + config: + app_name: "TestApp" + version: "1.0.0" + modules: + - | + package test.config + # Regular rule - should be accessible via virtual lookup + application_info := {"name": data.config.app_name, "version": data.config.version} + + # Function rule - should NOT be accessible via virtual lookup + format_version(major, minor) := sprintf("%d.%d", [major, minor]) + + # Another regular rule - should be accessible + app_status := "running" + - | + package test + main := result if { + # This should only include regular rules, not function rules + # data.test.config should contain: application_info, app_status + # but NOT: format_version (because it's a function rule) + result := data.test.config + } + query: data.test.main + want_result: + application_info: {"name": "TestApp", "version": "1.0.0"} + app_status: "running" + # Note: format_version should NOT appear here since it's a function rule \ No newline at end of file diff --git a/tests/rvm/rego/mod.rs b/tests/rvm/rego/mod.rs new file mode 100644 index 0000000..35207f7 --- /dev/null +++ b/tests/rvm/rego/mod.rs @@ -0,0 +1,578 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![cfg(feature = "rvm")] + +use anyhow::Result; +use regorus::languages::rego::compiler::Compiler; +use regorus::rvm::program::{generate_tabular_assembly_listing, AssemblyListingConfig, Program}; +use regorus::rvm::tests::test_utils::test_round_trip_serialization; +use regorus::rvm::vm::RegoVM; +use regorus::test_utils::{check_output, process_value, value_or_vec_to_vec, ValueOrVec}; +use regorus::{CompiledPolicy, Engine, Rc, Value}; +use serde::{Deserialize, Serialize}; +use std::fs; +use test_generator::test_resources; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct TestCase { + pub data: Option, + pub input: Option, + pub modules: Vec, + pub note: String, + pub query: String, + pub entry_points: Option>, + pub sort_bindings: Option, + pub want_result: Option, + pub want_results: Option>, + pub want_prints: Option>, + pub no_result: Option, + pub skip: Option, + pub error: Option, + pub traces: Option, + pub want_error: Option, + pub want_error_code: Option, + #[serde(default = "default_strict")] + pub strict: bool, + pub allow_interpreter_success: Option, + pub allow_interpreter_incorrect_behavior: Option, +} + +fn default_strict() -> bool { + true +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct YamlTest { + pub cases: Vec, +} + +fn render_program_listing(program: &Program) -> String { + let config = AssemblyListingConfig::default(); + generate_tabular_assembly_listing(program, &config) +} + +fn dump_rvm_listing(case_note: &str, listing: &Option) { + if let Some(listing) = listing { + eprintln!("\n===== RVM assembly for '{}' =====", case_note); + eprintln!("{}", listing); + eprintln!("===== End RVM assembly =====\n"); + } +} + +macro_rules! panic_with_listing { + ($listing:expr, $case_note:expr, $($arg:tt)*) => {{ + dump_rvm_listing($case_note, $listing); + panic!($($arg)*); + }}; +} + +macro_rules! bail_with_listing { + ($listing:expr, $case_note:expr, $($arg:tt)*) => {{ + dump_rvm_listing($case_note, $listing); + anyhow::bail!($($arg)*); + }}; +} + +fn should_run_test_case(case_note: &str) -> bool { + if let Ok(filter) = std::env::var("TEST_CASE_FILTER") { + case_note.contains(&filter) + } else { + true + } +} + +fn compile_and_run_rvm( + compiled_policy: &CompiledPolicy, + entrypoint: &str, + data: &Value, + input: &Value, + listing_out: &mut Option, +) -> anyhow::Result { + let results = compile_and_run_rvm_with_all_entry_points( + compiled_policy, + &[entrypoint], + data, + input, + listing_out, + )?; + results + .into_iter() + .next() + .ok_or_else(|| anyhow::anyhow!("no result returned from VM")) +} + +fn compile_and_run_rvm_with_entry_points( + compiled_policy: &CompiledPolicy, + entry_points: &[&str], + execute_entry_point: &str, + data: &Value, + input: &Value, + listing_out: &mut Option, +) -> anyhow::Result { + let results = compile_and_run_rvm_with_all_entry_points( + compiled_policy, + entry_points, + data, + input, + listing_out, + )?; + + if let Some(index) = entry_points + .iter() + .position(|ep| *ep == execute_entry_point) + { + results + .get(index) + .cloned() + .ok_or_else(|| anyhow::anyhow!("missing entry point result")) + } else { + Err(anyhow::anyhow!( + "entry point '{}' not found in {:?}", + execute_entry_point, + entry_points + )) + } +} + +fn compile_and_run_rvm_with_all_entry_points( + compiled_policy: &CompiledPolicy, + entry_points: &[&str], + data: &Value, + input: &Value, + listing_out: &mut Option, +) -> anyhow::Result> { + let program = Compiler::compile_from_policy(compiled_policy, entry_points)?; + + // Basic serialization sanity check keeps regressions visible in CI. + test_round_trip_serialization(program.as_ref()).map_err(|e| anyhow::anyhow!(e))?; + + *listing_out = Some(render_program_listing(program.as_ref())); + + let mut vm = RegoVM::new(); + vm.load_program(program); + vm.set_data(data.clone())?; + vm.set_input(input.clone()); + + let mut results = Vec::new(); + for (idx, _) in entry_points.iter().enumerate() { + let result = if entry_points.len() == 1 { + vm.execute()? + } else { + vm.execute_entry_point_by_index(idx)? + }; + results.push(result); + } + + Ok(results) +} + +fn yaml_test_impl(file: &str) -> Result<()> { + let yaml_str = fs::read_to_string(file)?; + let test: YamlTest = serde_yaml::from_str(&yaml_str)?; + + println!("running {file}"); + if let Ok(filter) = std::env::var("TEST_CASE_FILTER") { + println!("๐Ÿ” Test case filter active: '{filter}'"); + } + + let mut executed_count = 0usize; + let mut skipped_count = 0usize; + + for case in test.cases { + let mut last_listing: Option = None; + if !should_run_test_case(&case.note) { + println!("case {} filtered out", case.note); + skipped_count += 1; + continue; + } + + print!("case {} ", case.note); + + if case.skip == Some(true) { + println!("skipped"); + skipped_count += 1; + continue; + } + + executed_count += 1; + + let mut engine = Engine::new(); + for (idx, module) in case.modules.iter().enumerate() { + engine.add_policy(format!("rego_{idx}"), module.clone())?; + } + + if let Some(data) = case.data { + engine.add_data(data)?; + } + + let input_value = case + .input + .clone() + .map(|i| match i { + ValueOrVec::Single(v) => v, + ValueOrVec::Many(_) => Value::Null, + }) + .unwrap_or(Value::Null); + + if case.input.is_some() { + engine.set_input(input_value.clone()); + } + + let entrypoint_ref = Rc::from(case.query.as_str()); + let compilation_result = engine.compile_with_entrypoint(&entrypoint_ref); + let data = engine.get_data(); + let interpreter_result = engine.eval_rule(case.query.clone()); + + if let Err(compilation_error) = &compilation_result { + if let (None, Some(expected_error)) = (&case.want_result, &case.want_error) { + let error_str = compilation_error.to_string(); + if error_str.contains(expected_error) { + println!( + "โœ“ RVM compilation error matches expected for case '{}'", + case.note + ); + println!("passed"); + continue; + } + + panic_with_listing!( + &last_listing, + &case.note, + "RVM compilation error does not match expected for case '{}':\nExpected: '{expected_error}'\nActual: '{error_str}'", + case.note + ); + } + + dump_rvm_listing(&case.note, &last_listing); + return Err(anyhow::anyhow!("Compilation failed: {compilation_error}")); + } + + let compiled_policy = compilation_result.unwrap(); + + if let Some(expected_results) = &case.want_results { + if case.want_result.is_some() { + bail_with_listing!( + &last_listing, + &case.note, + "Cannot specify both want_result and want_results for case '{}'", + case.note + ); + } + if case.want_error.is_some() { + bail_with_listing!( + &last_listing, + &case.note, + "Cannot specify both want_results and want_error for case '{}'", + case.note + ); + } + + if let Some(ref entry_points) = case.entry_points { + let entry_point_refs: Vec<&str> = entry_points.iter().map(|s| s.as_str()).collect(); + match compile_and_run_rvm_with_all_entry_points( + &compiled_policy, + &entry_point_refs, + &data, + &input_value, + &mut last_listing, + ) { + Ok(actual_results) => { + if actual_results.len() != expected_results.len() { + bail_with_listing!( + &last_listing, + &case.note, + "Expected {} results, but got {} for case '{}'", + expected_results.len(), + actual_results.len(), + case.note + ); + } + + for (index, (actual, expected)) in actual_results + .iter() + .zip(expected_results.iter()) + .enumerate() + { + let expected_value = match expected { + ValueOrVec::Single(v) => v.clone(), + ValueOrVec::Many(vec) if vec.len() == 1 => vec[0].clone(), + ValueOrVec::Many(_) => { + bail_with_listing!( + &last_listing, + &case.note, + "Unexpected multiple expected values for result {} in case '{}'", + index, + case.note + ); + } + }; + + let processed_expected = process_value(&expected_value)?; + if *actual != processed_expected { + bail_with_listing!( + &last_listing, + &case.note, + "Result {} mismatch for case '{}': expected {:?}, got {:?}", + index, + case.note, + processed_expected, + actual + ); + } + } + + println!( + "โœ“ All {} entry point results match expected values for case '{}'", + actual_results.len(), + case.note + ); + continue; + } + Err(e) => { + bail_with_listing!( + &last_listing, + &case.note, + "Multiple entry points execution failed for case '{}': {}", + case.note, + e + ); + } + } + } else { + bail_with_listing!( + &last_listing, + &case.note, + "want_results specified but no entry_points provided for case '{}'", + case.note + ); + } + } + + match (&case.want_result, &case.want_error) { + (Some(expected_result), None) => { + let result = if let Some(ref entry_points) = case.entry_points { + let refs: Vec<&str> = entry_points.iter().map(|s| s.as_str()).collect(); + compile_and_run_rvm_with_entry_points( + &compiled_policy, + &refs, + &case.query, + &data, + &input_value, + &mut last_listing, + ) + } else { + compile_and_run_rvm( + &compiled_policy, + &case.query, + &data, + &input_value, + &mut last_listing, + ) + }; + + match result { + Ok(actual_result) => { + match &interpreter_result { + Ok(interpreter_value) => { + if actual_result != *interpreter_value { + if case.allow_interpreter_incorrect_behavior == Some(true) { + println!( + "โœ“ RVM result differs from interpreter for case '{}' (allowed)", + case.note + ); + } else { + panic_with_listing!( + &last_listing, + &case.note, + "RVM result does not match interpreter result for case '{}':\nRVM: {:?}\nInterpreter: {:?}", + case.note, + actual_result, + interpreter_value + ); + } + } + } + Err(err) => { + panic_with_listing!( + &last_listing, + &case.note, + "Interpreter failed for case '{}' but RVM succeeded:\nRVM result: {:?}\nInterpreter error: {}", + case.note, + actual_result, + err + ); + } + } + + let expected_results = value_or_vec_to_vec(expected_result.clone()); + let actual_results = vec![actual_result]; + check_output(&actual_results, &expected_results)?; + } + Err(e) => match &interpreter_result { + Ok(interpreter_value) => { + if case.allow_interpreter_success == Some(true) { + println!( + "โœ“ RVM detected conflict for case '{}' (interpreter success allowed): {}", + case.note, + e + ); + } else { + panic_with_listing!( + &last_listing, + &case.note, + "RVM failed for case '{}' but interpreter succeeded:\nRVM error: {}\nInterpreter result: {:?}", + case.note, + e, + interpreter_value + ); + } + } + Err(err) => { + panic_with_listing!( + &last_listing, + &case.note, + "Both RVM and interpreter failed for case '{}' but a result was expected:\nInterpreter error: {:?}\nRVM error: {}", + case.note, + err, + e + ); + } + }, + } + } + (None, Some(expected_error)) => { + let result = if let Some(ref entry_points) = case.entry_points { + let refs: Vec<&str> = entry_points.iter().map(|s| s.as_str()).collect(); + compile_and_run_rvm_with_entry_points( + &compiled_policy, + &refs, + &case.query, + &data, + &input_value, + &mut last_listing, + ) + } else { + compile_and_run_rvm( + &compiled_policy, + &case.query, + &data, + &input_value, + &mut last_listing, + ) + }; + + match result { + Ok(result) => match &interpreter_result { + Ok(interpreter_value) => { + panic_with_listing!( + &last_listing, + &case.note, + "Test case '{}' expected error '{}' but both RVM and interpreter succeeded:\nRVM result: {}\nInterpreter result: {:?}", + case.note, + expected_error, + serde_json::to_string_pretty(&result)?, + interpreter_value + ); + } + Err(_) => { + panic_with_listing!( + &last_listing, + &case.note, + "Test case '{}' expected error '{}' but RVM succeeded while interpreter failed:\nRVM result: {}", + case.note, + expected_error, + serde_json::to_string_pretty(&result)? + ); + } + }, + Err(actual_error) => match &interpreter_result { + Ok(interpreter_value) => { + if case.allow_interpreter_success == Some(true) { + let actual_error_str = actual_error.to_string(); + if !actual_error_str.contains(expected_error) { + panic_with_listing!( + &last_listing, + &case.note, + "Error message mismatch for case '{}': expected contains '{}', actual '{}'", + case.note, + expected_error, + actual_error_str + ); + } + println!( + "โœ“ RVM error matches expected for case '{}' (interpreter success allowed)", + case.note + ); + } else { + panic_with_listing!( + &last_listing, + &case.note, + "RVM failed for case '{}' but interpreter succeeded:\nRVM error: {}\nInterpreter result: {:?}", + case.note, + actual_error, + interpreter_value + ); + } + } + Err(_) => { + let actual_error_str = actual_error.to_string(); + if !actual_error_str.contains(expected_error) { + panic_with_listing!( + &last_listing, + &case.note, + "Error message mismatch for case '{}': expected contains '{}', actual '{}'", + case.note, + expected_error, + actual_error_str + ); + } + println!("โœ“ RVM error matches expected for case '{}'", case.note); + } + }, + } + } + _ => { + panic_with_listing!( + &last_listing, + &case.note, + "Test case '{}' must specify either want_result or want_error", + case.note + ); + } + } + + println!("passed"); + } + + println!( + "๐Ÿ“Š Test Summary for {}: {} executed, {} skipped", + file, executed_count, skipped_count + ); + + Ok(()) +} + +#[test_resources("tests/rvm/rego/cases/*.yaml")] +fn run_rego_compiler_yaml(file: &str) { + yaml_test_impl(file).unwrap(); +} + +#[test] +fn test_specific_case() { + if std::env::var("TEST_CASE_FILTER").is_err() { + println!("๐Ÿ’ก Specific case test skipped - no TEST_CASE_FILTER set"); + println!(" Usage: TEST_CASE_FILTER=\"note substring\" cargo test test_specific_case -- --nocapture"); + return; + } + + if let Ok(entries) = fs::read_dir("tests/rvm/rego/cases") { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) == Some("yaml") { + if let Err(e) = yaml_test_impl(path.to_str().unwrap()) { + println!("โŒ Error in file {}: {}", path.display(), e); + } + } + } + } +}