feat!: Rego -> RVM Compiler and extensive testsuite (#506)

# 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 <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2025-11-24 12:08:37 -06:00
committed by GitHub
parent 688e6128d4
commit a3a20a1235
43 changed files with 6945 additions and 139 deletions

View File

@@ -25,7 +25,7 @@ pub(crate) type InferredResourceTypes = BTreeMap<Ref<Query>, ResourceTypeInfo>;
/// Wrapper around CompiledPolicyData that holds an Rc reference.
#[derive(Debug, Clone)]
pub struct CompiledPolicy {
inner: Rc<CompiledPolicyData>,
pub(crate) inner: Rc<CompiledPolicyData>,
}
impl CompiledPolicy {
@@ -33,6 +33,21 @@ impl CompiledPolicy {
pub(crate) fn new(inner: Rc<CompiledPolicyData>) -> 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<String, Vec<Ref<Rule>>> {
&self.inner.rules
}
/// Get access to the modules in the compiled policy.
pub fn get_modules(&self) -> &Vec<Ref<Module>> {
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 {

View File

@@ -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<Value> {
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,

View File

@@ -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<Register> {
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<Register> {
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<Register> {
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<Register> {
self.compile_comprehension(
ComprehensionMode::Object,
ComprehensionType::Object,
Some(key),
Some(value),
query,
span,
)
}
}

View File

@@ -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<u16> {
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<CompilationContext> {
// 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<Register> {
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<Register> {
self.lookup_local_var(var_name)
}
pub(super) fn get_binding_plan_for_expr(&self, expr: &ExprRef) -> Option<BindingPlan> {
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<BindingPlan> {
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<Register> {
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
}
}
}

View File

@@ -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<Register> {
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<Register>,
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(())
}
}

View File

@@ -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<anyhow::Error> for CompilerError {
fn from(err: anyhow::Error) -> Self {
CompilerError::General {
message: format!("{}", err),
}
}
}
pub type Result<T> = ::core::result::Result<T, CompilerError>;

View File

@@ -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<Register> {
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<Register> {
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<Register> = 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)
}
}

View File

@@ -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<Register> {
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<Register> {
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<Register> {
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<Value> = 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)
}
}

View File

@@ -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<Register> {
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<Register> {
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<Register> {
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<Register> {
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<Register> {
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<Register> {
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)
}
}

View File

@@ -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<Register> {
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, &reg) 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, &reg) 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)
}
}

View File

@@ -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<Vec<HoistedLoop>> {
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<HoistedLoop> {
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(
&current_loop.loop_expr,
&current_loop.key,
&current_loop.value,
&current_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<Span>,
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<ExprRef>,
key_var: &Option<ExprRef>,
_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<ExprRef>,
value: &ExprRef,
collection: &ExprRef,
remaining_stmts: &[&LiteralStmt],
) -> Result<Register> {
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<ExprRef>,
value: &ExprRef,
collection: &ExprRef,
loop_body_stmts: &[&LiteralStmt],
) -> Result<Register> {
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)
}
}

View File

@@ -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<String, Register>,
unbound_vars: BTreeSet<String>,
}
#[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<ExprRef>,
pub(super) value_expr: Option<ExprRef>,
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<u16>,
}
impl WorklistEntry {
pub fn new(rule_path: String, call_stack: Vec<u16>) -> 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<SpanInfo>,
register_counter: Register,
scopes: Vec<Scope>,
policy: &'a CompiledPolicy,
current_package: String,
current_module_index: u32,
rule_index_map: BTreeMap<String, u16>,
rule_worklist: Vec<WorklistEntry>,
rule_definitions: Vec<Vec<Vec<u32>>>,
rule_definition_function_params: Vec<Vec<Option<Vec<String>>>>,
rule_definition_destructuring_patterns: Vec<Vec<Option<u32>>>,
rule_types: Vec<RuleType>,
rule_function_param_count: Vec<Option<usize>>,
rule_result_registers: Vec<u8>,
rule_num_registers: Vec<u8>,
context_stack: Vec<CompilationContext>,
loop_expr_register_map: BTreeMap<ExprRef, Register>,
source_to_index: BTreeMap<String, usize>,
builtin_index_map: BTreeMap<String, u16>,
current_input_register: Option<Register>,
current_data_register: Option<Register>,
current_rule_path: String,
current_call_stack: Vec<u16>,
entry_points: IndexMap<String, usize>,
}
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(),
}
}
}

View File

@@ -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<Program> {
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<u16> = 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<u16> {
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<Option<u32>> {
self.rule_definition_destructuring_patterns[rule_index as usize].clone()
}
}

View File

@@ -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(())
}
}

View File

@@ -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<AccessComponent>,
}
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<ReferenceChain> {
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<Register> {
// 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<Register> {
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<Register> {
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<String> = 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<Register> {
// 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<Register> {
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<Register> {
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)
}
}

View File

@@ -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<RuleType> {
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<RuleType> = 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<u16> {
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<Rule>) -> Result<u32> {
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<String, Vec<crate::ast::NodeRef<Rule>>>,
) -> 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<Arc<Program>> {
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<String, Vec<crate::ast::NodeRef<Rule>>>,
) -> 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<String, Vec<crate::ast::NodeRef<Rule>>>,
) -> 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<usize> = 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<Span> = 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<String> =
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(())
}
}

View File

@@ -0,0 +1,2 @@
#[path = "compiler/mod.rs"]
pub mod compiler;

View File

@@ -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;

View File

@@ -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};

127
src/test_utils.rs Normal file
View File

@@ -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<Value>),
}
impl Serialize for ValueOrVec {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<Value> {
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<Value> {
match value_or_vec {
ValueOrVec::Single(single_result) => vec![single_result],
ValueOrVec::Many(many_result) => many_result,
}
}

188
src/tests/common.rs Normal file
View File

@@ -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<Value> {
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<Value>),
}
impl Serialize for ValueOrVec {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<Value>,
pub input: Option<ValueOrVec>,
pub modules: Vec<String>,
pub note: String,
pub query: String,
pub entry_points: Option<Vec<String>>,
pub sort_bindings: Option<bool>,
pub want_result: Option<ValueOrVec>,
pub want_results: Option<Vec<ValueOrVec>>,
pub want_prints: Option<Vec<String>>,
pub no_result: Option<bool>,
pub skip: Option<bool>,
pub error: Option<String>,
pub traces: Option<bool>,
pub want_error: Option<String>,
pub want_error_code: Option<String>,
#[serde(default = "default_strict")]
pub strict: bool,
/// Allow interpreter to succeed when RVM fails with conflict detection
pub allow_interpreter_success: Option<bool>,
/// Allow interpreter to produce incorrect results when RVM produces correct results
pub allow_interpreter_incorrect_behavior: Option<bool>,
}
fn default_strict() -> bool {
true
}
/// Standard YAML test file structure
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct YamlTest {
pub cases: Vec<TestCase>,
}
/// Convert ValueOrVec to a vector of Values
pub fn value_or_vec_to_vec(value_or_vec: ValueOrVec) -> Vec<Value> {
match value_or_vec {
ValueOrVec::Single(single_result) => vec![single_result],
ValueOrVec::Many(many_result) => many_result,
}
}

View File

@@ -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<Value> {
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<Value>) {
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<Value>),
}
impl Serialize for ValueOrVec {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<Value>,

View File

@@ -8,3 +8,6 @@ mod engine;
mod lexer;
mod parser;
mod value;
#[cfg(feature = "rvm")]
mod rvm;

3
tests/rvm/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
mod rego;

View File

@@ -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

View File

@@ -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]]

View File

@@ -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"

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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}

View File

@@ -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"

View File

@@ -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!: []

View File

@@ -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"]

View File

@@ -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"

View File

@@ -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

578
tests/rvm/rego/mod.rs Normal file
View File

@@ -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<Value>,
pub input: Option<ValueOrVec>,
pub modules: Vec<String>,
pub note: String,
pub query: String,
pub entry_points: Option<Vec<String>>,
pub sort_bindings: Option<bool>,
pub want_result: Option<ValueOrVec>,
pub want_results: Option<Vec<ValueOrVec>>,
pub want_prints: Option<Vec<String>>,
pub no_result: Option<bool>,
pub skip: Option<bool>,
pub error: Option<String>,
pub traces: Option<bool>,
pub want_error: Option<String>,
pub want_error_code: Option<String>,
#[serde(default = "default_strict")]
pub strict: bool,
pub allow_interpreter_success: Option<bool>,
pub allow_interpreter_incorrect_behavior: Option<bool>,
}
fn default_strict() -> bool {
true
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct YamlTest {
pub cases: Vec<TestCase>,
}
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<String>) {
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<String>,
) -> anyhow::Result<Value> {
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<String>,
) -> anyhow::Result<Value> {
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<String>,
) -> anyhow::Result<Vec<Value>> {
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<String> = 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);
}
}
}
}
}