mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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:
committed by
GitHub
parent
688e6128d4
commit
a3a20a1235
+16
-1
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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, ®) in arg_regs.iter().take(8).enumerate() {
|
||||
args_array[i] = reg;
|
||||
}
|
||||
|
||||
let params_index = self.program.add_function_call_params(FunctionCallParams {
|
||||
func_rule_index: rule_index,
|
||||
dest,
|
||||
num_args,
|
||||
args: args_array,
|
||||
});
|
||||
self.emit_instruction(Instruction::FunctionCall { params_index }, &span);
|
||||
} else if self.is_builtin(&original_fcn_path) {
|
||||
let builtin_index = self.get_builtin_index(&original_fcn_path)?;
|
||||
let mut args_array = [0u8; 8];
|
||||
let num_args = arg_regs.len().min(8) as u8;
|
||||
for (i, ®) in arg_regs.iter().take(8).enumerate() {
|
||||
args_array[i] = reg;
|
||||
}
|
||||
|
||||
let params_index = self.program.add_builtin_call_params(BuiltinCallParams {
|
||||
dest,
|
||||
builtin_index,
|
||||
num_args,
|
||||
args: args_array,
|
||||
});
|
||||
self.emit_instruction(Instruction::BuiltinCall { params_index }, &span);
|
||||
} else {
|
||||
return Err(CompilerError::UnknownFunction {
|
||||
name: original_fcn_path,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(dest)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
¤t_loop.loop_expr,
|
||||
¤t_loop.key,
|
||||
¤t_loop.value,
|
||||
¤t_loop.collection,
|
||||
stmts,
|
||||
remaining_loops,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
LoopType::Walk => Err(CompilerError::General {
|
||||
message: "walk loops are not yet supported in the RVM compiler".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn compile_every_quantifier(
|
||||
&mut self,
|
||||
key: &Option<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)
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#[path = "compiler/mod.rs"]
|
||||
pub mod compiler;
|
||||
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user