Files
regorus/src/interpreter.rs
Anand Krishnamoorthi 83891d7782 RVM compiler & runtime optimizations: caching, instruction fusion, constant hoisting, and correctness fixes (#626)
* perf!: add LRU caches for compiled regex and glob patterns

Add bounded LRU caches for compiled regex and glob patterns used by
Rego builtins, avoiding repeated recompilation of the same patterns
during policy evaluation.

New `cache` feature (included in `full-opa` and `opa-no-std`) backed by
the `lru` crate (no_std compatible) with `spin::Mutex` for thread safety.

- `src/cache.rs`: generic `LruCache<V>` wrapper, global `REGEX_CACHE`
  (default capacity 256) and `GLOB_CACHE` (default capacity 128)
- `src/builtins/regex.rs`: all regex builtins route through the cache
- `src/builtins/glob.rs`: glob.match routes through the cache
- Public API: `regorus::cache::{Config, configure, clear}`

Compilation costs avoided per cache hit:
  regex  10-55 µs  (simple to complex patterns)
  glob   10-12 µs
  LRU hit   ~10 ns

BREAKING CHANGE: new `cache` Cargo feature added to `full-opa` and
`opa-no-std` feature sets; adds `lru` as a dependency.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(vm): amortize per-instruction memory and time limit checks

Deduplicate per-instruction memory_check calls by hoisting them to the
main dispatch loop, and amortize monotonic_now() syscalls in the
execution timer by checking elapsed time every N instructions instead
of on every tick.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix(vm): correct object membership to check values only, not keys

The Contains instruction for objects was checking both keys and values:

    object_fields.contains_key(v) || object_fields.values().any(|v| ...)

Per the Rego specification, `x in obj` tests whether x is a VALUE of
the object, not a key. The two-argument form `k, v in obj` is needed
to access keys. The interpreter already implemented this correctly
(values-only scan), but the RVM had the extra contains_key() check
which would incorrectly return true when the search value happened to
match a key name.

Remove the contains_key() branch so the behavior matches the interpreter
and the Rego spec. Add two regression tests:
- object_membership_checks_values_not_keys: "foo" in {"foo": "bar"}
  must be false (key, not a value)
- object_membership_finds_value: "bar" in {"foo": "bar"} must be true

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(compiler): hoist all-constant collection literals to the literal table

When an array, set, or object literal consists entirely of compile-time
constant expressions (numbers, strings, bools, null, and nested constant
collections), the compiler now evaluates them at compile time and emits a
single Load instruction from the literal table instead of generating
per-element instructions at runtime.

Previously, a Rego expression like `x in [1, 2, 3]` would emit
ArrayCreate + three Load + three ArrayAppend instructions, allocating a
new Vec and Rc on every evaluation. With this change, the entire array
is built once during compilation and loaded as a single constant.

This optimization applies to all three collection types:
- Array literals: avoids ArrayCreate + N x (Load + ArrayAppend)
- Set literals: avoids SetCreate + N x (Load + SetAdd)
- Object literals: avoids ObjectCreate + N x (Load + Load + ObjectInsert)

The implementation adds a try_eval_const() helper that recursively
evaluates an AST expression as a constant Value, returning None if any
sub-expression is non-constant. Each compile method for collection
literals attempts the all-constant fast path first and falls through to
the existing instruction-by-instruction codegen otherwise.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(compiler): fuse Eq + AssertCondition into AssertEq instruction

Add a new `AssertEq { left, right }` instruction that combines equality
comparison and condition assertion into a single operation. This replaces
the previous two-instruction pattern of `Eq { dest, left, right }` followed
by `AssertCondition { condition: dest }`, saving one instruction and one
register per equality assertion.

The fused instruction checks two registers for equality and directly calls
handle_condition with the result, avoiding the intermediate boolean
register entirely. If either operand is undefined or the values differ,
the condition fails and the rule/loop backtracks.

The optimization applies to four destructuring sites:
- EqualityCheck (assignment re-binding with `x = expr; x = expr`)
- EqualityExpr (destructuring against an expression)
- EqualityValue (destructuring against a literal value)
- assert_array_length (array length validation in destructuring)

In soft_assert_mode the compiler still emits the original Eq instruction
since the boolean result register is needed by callers.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(compiler): fuse Not + AssertCondition into AssertNot instruction

Add a new `AssertNot { operand }` instruction that combines logical
negation and condition assertion into a single operation. This replaces
the previous two-instruction pattern of `Not { dest, operand }` followed
by `AssertCondition { condition: dest }`, saving one instruction and one
register allocation.

The fused instruction checks the operand register and passes the
condition if the value is false or undefined (per Rego semantics where
`not expr` succeeds when the expression has no results or is false),
and fails the condition if the value is true or any non-boolean truthy
value.

This was the only emission site for the Not+AssertCondition pair,
occurring in the compilation of `Literal::NotExpr` statements.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(vm): early exit for same-value multi-definition rules

When a rule has multiple definitions that all produce the same value
(e.g. implicit true, or identical literal), set early_exit_on_first_success
on RuleInfo so the VM can stop after the first successful definition.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* feat!: expose cache configuration API to all language bindings

Add `set_cache_config` and `clear_cache` functions to every binding
so callers can tune or reset the global regex/glob pattern caches
introduced in the cache feature.

Bindings updated:
- FFI (C): `regorus_set_cache_config`, `regorus_clear_cache`
- C++ header: free functions `regorus::set_cache_config`, `regorus::clear_cache`
- Python: module-level `set_cache_config(*, regex, glob)`, `clear_cache()`
- Java: static methods on new `CacheConfig` class
- Go: package-level `SetCacheConfig`, `ClearCache`
- Ruby: module functions `Regorus.set_cache_config`, `Regorus.clear_cache`
- WASM: free functions `setCacheConfig`, `clearCache`
- C#: static methods `Engine.SetCacheConfig`, `Engine.ClearCache`

BREAKING CHANGE: Bump SERIALIZATION_VERSION from 4 to 5 due to new
AssertEq and AssertNot instruction variants added in the instruction
fusion commits. Programs serialized with version 5 cannot be loaded
by older versions of regorus.

* fix: address PR review feedback

Cache subsystem:
- Gate REGEX_CACHE and related imports behind #[cfg(feature = "regex")]
  so that building with --features cache without regex compiles correctly.
- Gate LruCache struct behind #[cfg(any(feature = "regex", feature = "glob"))].
- Add Config::MAX_CAPACITY (2^16) hard upper bound; clamp values in
  configure() to prevent unbounded cache growth.
- Use parking_lot::Mutex for std builds and spin::Mutex for no_std to
  avoid CPU spinning under contention in tight regex/glob eval loops.
- Narrow lock scopes in regex/glob builtins: release the mutex before
  compiling a pattern, then re-acquire to insert.

Java JNI binding:
- Fix cache config overflow: negative jlong values now saturate to 0
  and positive overflow saturates to usize::MAX (then clamped by
  MAX_CAPACITY) instead of silently disabling the cache.
- Gate JNI cache config/clear functions behind #[cfg(feature = "cache")].

Compiler:
- Refactor static_value_of_expr to delegate to try_eval_const,
  gaining support for negated numbers and constant collections.
- Make try_eval_const pub(in crate::languages::rego::compiler) and
  re-export through expressions.rs.
- Handle Expr::UnaryExpr with numeric literals in try_eval_const so
  collections containing negated numbers (e.g. [-1, 2]) are hoisted.

VM correctness:
- Fix Not instruction to follow Rego semantics: not expr yields
  true when expr is undefined or false, false for any other defined
  value (including non-booleans) -- no longer errors on non-boolean
  operands.
- Add enforce_memory_check() call at execute_suspendable_entry to
  ensure memory limits are checked before the first instruction.
- Update AssertNot listing comment to "exit if any defined truthy
  value" to match actual VM behaviour.
- Add doc comment on Not instruction clarifying Rego negation
  semantics.

Bindings:
- Fix C++ header indentation for set_cache_config / clear_cache.
- Propagate Cargo.lock parking_lot addition across ffi, java, python,
  and wasm binding lockfiles.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-23 21:00:51 -05:00

4434 lines
162 KiB
Rust

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(clippy::print_stderr, clippy::pattern_type_mismatch)]
use crate::ast::*;
use crate::builtins::{self, BuiltinFcn};
use crate::compiled_policy::CompiledPolicyData;
#[cfg(feature = "azure_policy")]
use crate::compiled_policy::TargetInfo;
use crate::compiler::destructuring_planner::{
AssignmentPlan, BindingPlan, DestructuringPlan, WildcardSide,
};
use crate::compiler::hoist::{HoistedLoop, LoopType};
use crate::lexer::*;
use crate::lookup::Lookup;
use crate::parser::Parser;
use crate::scheduler::*;
use crate::utils::limits::{monotonic_now, ExecutionTimer, ExecutionTimerConfig};
#[cfg(feature = "std")]
use crate::utils::*;
#[cfg(not(feature = "std"))]
use crate::utils::{get_extra_arg, get_path_string, get_root_var, FunctionTable};
use crate::value::*;
use crate::*;
use crate::{Expression, Extension, Location, QueryResult, QueryResults};
#[cfg(feature = "coverage")]
use crate::query::traversal::traverse;
use crate::Rc;
use alloc::collections::btree_map::Entry as BTreeMapEntry;
use alloc::collections::{BTreeMap, BTreeSet};
use anyhow::{anyhow, bail, Result};
use core::ops::Bound::*;
type Scope = BTreeMap<SourceStr, Value>;
type ExprLookup = Lookup<Value>;
#[cfg(feature = "azure_policy")]
pub mod error;
#[cfg(feature = "azure_policy")]
pub mod target {
pub mod infer;
pub mod resolve;
}
type ContextExprs = (Option<Ref<Expr>>, Option<Ref<Expr>>);
type State = (
Value,
Value,
Value,
BTreeSet<Ref<Rule>>,
Value,
BTreeMap<String, FunctionModifier>,
RuleValues,
);
#[derive(Debug, Clone)]
enum FunctionModifier {
Function(String),
Value(Value),
}
type RuleValues = BTreeMap<Vec<Value>, (Value, Ref<Expr>)>;
#[derive(Debug)]
pub struct Interpreter {
compiled_policy: Rc<CompiledPolicyData>,
data: Value,
#[cfg(feature = "coverage")]
coverage: Map<Source, Vec<bool>>,
#[cfg(feature = "coverage")]
enable_coverage: bool,
traces: Option<Vec<Rc<str>>>,
gather_prints: bool,
prints: Vec<String>,
extensions: Map<String, (u8, Rc<Box<dyn Extension>>)>,
module: Option<Ref<Module>>,
current_module_path: String,
current_module_index: u32,
query_schedule: Option<Schedule>,
query_module: Option<NodeRef<Module>>,
input: Value,
init_data: Value,
with_document: Value,
with_functions: BTreeMap<String, FunctionModifier>,
scopes: Vec<Scope>,
// TODO: handle recursive calls where same expr could have different values.
loop_var_values: ExprLookup,
contexts: Vec<Context>,
processed: BTreeSet<Ref<Rule>>,
processed_paths: Value,
rule_values: RuleValues,
active_rules: Vec<Ref<Rule>>,
builtins_cache: BTreeMap<(&'static str, Vec<Value>), Value>,
no_rules_lookup: bool,
execution_timer: ExecutionTimer,
}
impl Default for Interpreter {
fn default() -> Self {
Self::new()
}
}
impl Clone for Interpreter {
fn clone(&self) -> Self {
Self {
compiled_policy: self.compiled_policy.clone(),
data: self.data.clone(),
init_data: self.init_data.clone(),
input: self.input.clone(),
with_document: self.with_document.clone(),
with_functions: self.with_functions.clone(),
gather_prints: self.gather_prints,
prints: self.prints.clone(),
traces: self.traces.clone(),
extensions: self.extensions.clone(),
#[cfg(feature = "coverage")]
coverage: self.coverage.clone(),
#[cfg(feature = "coverage")]
enable_coverage: self.enable_coverage,
// The following fields always get cleared during an evaluation.
// Hence, they need not be copied.
processed: BTreeSet::default(),
processed_paths: Value::new_object(),
loop_var_values: ExprLookup::new(),
scopes: Vec::default(),
rule_values: BTreeMap::default(),
builtins_cache: BTreeMap::default(),
active_rules: Vec::default(),
contexts: Vec::default(),
current_module_path: String::default(),
current_module_index: 0,
query_schedule: None,
query_module: None,
module: None,
no_rules_lookup: false,
execution_timer: ExecutionTimer::new(self.execution_timer.config()),
}
}
}
#[derive(Debug, Clone)]
struct Context {
key_expr: Option<ExprRef>,
output_expr: Option<ExprRef>,
value: Value,
result: Option<QueryResult>,
results: QueryResults,
is_compr: bool,
rule_ref: Option<ExprRef>,
rule_value: Value,
is_set: bool,
is_old_style_set: bool,
output_constness_determined: bool,
early_return: bool,
}
impl Default for Context {
fn default() -> Self {
Self {
key_expr: None,
output_expr: None,
value: Value::Undefined,
result: None,
results: QueryResults::default(),
is_compr: false,
rule_ref: None,
rule_value: Value::new_object(),
is_set: false,
is_old_style_set: false,
output_constness_determined: false,
early_return: false,
}
}
}
impl Interpreter {
pub fn new() -> Interpreter {
let compiled_policy = compiled_policy::CompiledPolicyData {
strict_builtin_errors: true, // Preserve current behavior
..Default::default()
};
Interpreter {
compiled_policy: Rc::new(compiled_policy),
data: Value::new_object(),
module: None,
current_module_path: String::default(),
current_module_index: 0,
query_schedule: None,
query_module: None,
input: Value::Undefined,
init_data: Value::new_object(),
with_document: Value::new_object(),
with_functions: BTreeMap::default(),
scopes: Vec::default(),
contexts: Vec::default(),
loop_var_values: Lookup::default(),
processed: BTreeSet::default(),
processed_paths: Value::new_object(),
rule_values: BTreeMap::default(),
active_rules: Vec::default(),
builtins_cache: BTreeMap::default(),
no_rules_lookup: false,
traces: None,
extensions: Map::default(),
#[cfg(feature = "coverage")]
coverage: Map::default(),
#[cfg(feature = "coverage")]
enable_coverage: false,
gather_prints: false,
prints: Vec::default(),
execution_timer: ExecutionTimer::new(None),
}
}
/// Create a new Interpreter from a compiled policy.
pub fn new_from_compiled_policy(compiled_policy: Rc<CompiledPolicyData>) -> Self {
Self {
data: Value::new_object(),
module: None,
current_module_path: String::default(),
current_module_index: 0,
query_module: None,
query_schedule: None,
input: Value::Undefined,
with_document: Value::new_object(),
with_functions: BTreeMap::default(),
scopes: Vec::default(),
contexts: Vec::default(),
loop_var_values: Lookup::default(),
processed: BTreeSet::default(),
processed_paths: Value::new_object(),
rule_values: BTreeMap::default(),
active_rules: Vec::default(),
builtins_cache: BTreeMap::default(),
no_rules_lookup: false,
traces: None,
#[cfg(feature = "coverage")]
coverage: Map::default(),
#[cfg(feature = "coverage")]
enable_coverage: false,
gather_prints: false,
prints: Vec::default(),
extensions: compiled_policy.extensions.clone(),
compiled_policy: compiled_policy.clone(),
init_data: compiled_policy
.data
.clone()
.unwrap_or_else(Value::new_object),
execution_timer: ExecutionTimer::new(None),
}
}
fn reset_execution_timer_state(&mut self) {
self.execution_timer.reset();
if self.execution_timer.limit().is_none() {
return;
}
if let Some(now) = monotonic_now() {
self.execution_timer.start(now);
}
}
fn execution_timer_tick(&mut self, work_units: u32) -> Result<()> {
if !self.execution_timer.accumulate(work_units) {
return Ok(());
}
let Some(now) = monotonic_now() else {
return Ok(());
};
self.execution_timer.check_now(now)?;
Ok(())
}
#[inline]
fn check_execution_time(&mut self) -> Result<()> {
self.execution_timer_tick(1)
}
fn compiled_policy_mut(&mut self) -> &mut CompiledPolicyData {
Rc::make_mut(&mut self.compiled_policy)
}
pub fn set_schedule(&mut self, schedule: Option<Rc<Schedule>>) {
self.compiled_policy_mut().schedule = schedule;
}
pub fn set_functions(&mut self, functions: FunctionTable) {
self.compiled_policy_mut().functions = functions;
}
pub fn set_modules(&mut self, modules: Rc<Vec<Ref<Module>>>) {
self.compiled_policy_mut().modules = modules;
}
pub fn set_loop_hoisting_table(&mut self, table: crate::compiler::hoist::HoistedLoopsLookup) {
self.compiled_policy_mut().loop_hoisting_table = table;
}
pub fn take_loop_hoisting_table(&mut self) -> crate::compiler::hoist::HoistedLoopsLookup {
core::mem::replace(
&mut self.compiled_policy_mut().loop_hoisting_table,
crate::compiler::hoist::HoistedLoopsLookup::new(),
)
}
pub const fn get_data_mut(&mut self) -> &mut Value {
&mut self.data
}
pub fn set_init_data(&mut self, data: Value) {
self.init_data = data;
}
pub const fn get_init_data(&self) -> &Value {
&self.init_data
}
pub const fn get_init_data_mut(&mut self) -> &mut Value {
&mut self.init_data
}
// Used by tests.
#[allow(dead_code)]
pub const fn get_compiled_policy(&self) -> &Rc<CompiledPolicyData> {
&self.compiled_policy
}
pub fn set_traces(&mut self, enable_tracing: bool) {
self.traces = match enable_tracing {
true => Some(vec![]),
false => None,
};
}
pub fn set_strict_builtin_errors(&mut self, b: bool) {
self.compiled_policy_mut().strict_builtin_errors = b;
}
pub fn set_execution_timer_config(&mut self, config: Option<ExecutionTimerConfig>) {
self.execution_timer = ExecutionTimer::new(config);
self.reset_execution_timer_state();
}
pub fn set_input(&mut self, input: Value) {
self.input = input.clone();
// Update with_document["input"] too, in case if engine is being reused and was already prepared
if let Ok(with_input) = Self::make_or_get_value_mut(&mut self.with_document, &["input"]) {
*with_input = input;
}
}
pub fn init_with_document(&mut self) -> Result<()> {
*Self::make_or_get_value_mut(&mut self.with_document, &["data"])? = self.init_data.clone();
*Self::make_or_get_value_mut(&mut self.with_document, &["input"])? = self.input.clone();
Ok(())
}
pub fn clear_builtins_cache(&mut self) {
self.builtins_cache.clear();
}
pub fn clean_internal_evaluation_state(&mut self) {
self.data = self.init_data.clone();
self.processed.clear();
self.processed_paths = Value::new_object();
self.ensure_loop_var_values_capacity();
self.scopes = vec![Scope::new()];
self.contexts = vec![];
self.rule_values.clear();
self.builtins_cache.clear();
self.reset_execution_timer_state();
}
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
fn memory_check(&mut self) -> Result<()> {
let _ = self; // quiet clippy::unused_self; retained for symmetry with VM path
crate::utils::limits::check_memory_limit_if_needed().map_err(|err| anyhow!(err))
}
#[cfg(any(miri, not(feature = "allocator-memory-limits")))]
const fn memory_check(&mut self) -> Result<()> {
let _ = self; // quiet clippy::unused_self; retained for symmetry with VM path
Ok(())
}
// Helper methods for working with ExprLookup
fn set_loop_var_value(&mut self, expr: &ExprRef, value: Value) -> Result<()> {
let module_idx = self.current_module_index;
let expr_idx = expr.eidx();
self.loop_var_values
.set_checked(module_idx, expr_idx, value)
.map_err(|err| anyhow!("internal error: loop var indices out of bounds: {err}"))
}
fn get_loop_var_value(&self, expr: &ExprRef) -> Result<Option<&Value>> {
let module_idx = self.current_module_index;
let expr_idx = expr.eidx();
self.loop_var_values
.get_checked(module_idx, expr_idx)
.map_err(|err| anyhow!("internal error: loop var indices out of bounds: {err}"))
}
fn remove_loop_var_value(&mut self, expr: &ExprRef) {
let module_idx = self.current_module_index;
let expr_idx = expr.eidx();
self.loop_var_values.clear(module_idx, expr_idx);
}
#[inline]
fn loop_assignment_expr(loop_info: &HoistedLoop) -> &ExprRef {
loop_info.loop_expr.as_ref().unwrap_or(&loop_info.value)
}
#[inline]
fn loop_index_expr(loop_info: &HoistedLoop) -> Option<&ExprRef> {
loop_info.key.as_ref().or(None)
}
#[inline]
const fn loop_collection_expr(loop_info: &HoistedLoop) -> &ExprRef {
&loop_info.collection
}
#[inline]
fn loop_span(loop_info: &HoistedLoop) -> Span {
loop_info.value.span().clone()
}
fn get_walk_binding_plan(&self, loop_info: &HoistedLoop) -> Result<Option<DestructuringPlan>> {
if loop_info.loop_type != LoopType::Walk {
return Ok(None);
}
if let Expr::Call { params, .. } = Self::loop_assignment_expr(loop_info).as_ref() {
if let Some(last_param) = params.last() {
let module_idx = self.current_module_index;
let expr_idx = last_param.as_ref().eidx();
let binding_plan = self
.compiled_policy
.loop_hoisting_table
.get_expr_binding_plan(module_idx, expr_idx)
.map_err(|err| anyhow!("loop hoisting table out of bounds: {err}"))?;
return match binding_plan.cloned() {
Some(BindingPlan::Parameter {
destructuring_plan, ..
}) => Ok(Some(destructuring_plan)),
Some(other_plan) => bail!(
"internal error: expected Parameter for walk output parameter, got {:?}",
other_plan
),
None => bail!("internal error: missing binding plan for walk output parameter"),
};
}
}
bail!("internal error: walk loop missing output parameter expression")
}
fn ensure_loop_var_values_capacity(&mut self) {
for (module_idx, module) in self.compiled_policy.modules.iter().enumerate() {
if let Ok(idx_u32) = u32::try_from(module_idx) {
self.loop_var_values
.ensure_capacity(idx_u32, module.num_expressions);
}
}
if let Some(query_module) = &self.query_module {
let query_module_idx = self.compiled_policy.modules.len();
let eidx = query_module.num_expressions;
if let Ok(idx_u32) = u32::try_from(query_module_idx) {
self.loop_var_values.ensure_capacity(idx_u32, eidx);
}
}
}
fn current_module(&self) -> Result<Ref<Module>> {
self.module
.clone()
.ok_or_else(|| anyhow!("internal error: current module not set"))
}
fn current_scope(&mut self) -> Result<&Scope> {
self.scopes
.last()
.ok_or_else(|| anyhow!("internal error: no active scope"))
}
fn current_scope_mut(&mut self) -> Result<&mut Scope> {
self.scopes
.last_mut()
.ok_or_else(|| anyhow!("internal error: no active scope"))
}
#[inline(always)]
fn add_variable(&mut self, name: &SourceStr, value: Value) -> Result<()> {
// Only add the variable if the key is not "_"
if name.text() != "_" {
self.current_scope_mut()?.insert(name.clone(), value);
}
Ok(())
}
fn add_variable_or(&mut self, name: &SourceStr) -> Result<Value> {
for scope in self.scopes.iter().rev() {
if let Some(variable) = scope.get(name) {
return Ok(variable.clone());
}
}
self.add_variable(name, Value::Undefined)?;
Ok(Value::Undefined)
}
fn eval_chained_ref_dot_or_brack(&mut self, mut expr: &ExprRef) -> Result<Value> {
// Collect a chaing of '.field' or '["field"]'
let mut path = vec![];
loop {
if let Some(v) = self.get_loop_var_value(expr)? {
path.reverse();
return Ok(Self::get_value_chained(v.clone(), &path[..]));
}
match expr.as_ref() {
// Stop path collection upon encountering the leading variable.
Expr::Var { span, .. } => {
path.reverse();
return self.lookup_var(span, &path[..], false);
}
// Accumulate chained . field accesses.
Expr::RefDot { refr, field, .. } => {
expr = refr;
path.push(field.0.text());
}
Expr::RefBrack { refr, index, .. } => match index.as_ref() {
// refr["field"] is the same as refr.field
Expr::String { span, .. } => {
expr = refr;
path.push(span.text());
}
// Handle other forms of refr.
// Note, we have the choice to evaluate a non-string index
_ => {
path.reverse();
let index = self.eval_expr(index)?;
// Handle indexing into data.
if let Ok(ref_path) = get_path_string(refr, None) {
if get_root_var(refr)?.text() == "data" && index != Value::Undefined {
let index = match &index {
Value::String(s) => s.to_string(),
_ => index.to_string(),
};
let ref_path = if path.is_empty() {
format!("{ref_path}.{index}")
} else {
format!("{ref_path}.{index}.{}", path.join("."))
};
self.ensure_rule_evaluated(ref_path)?;
}
}
let obj = self.eval_expr(refr)?;
let mut v = obj[&index].clone();
// Qualified references starting with data (e.g data.p.q) can
// be indexed using numbers. The number will be converted to string
// if a matching key exists.
if v == Value::Undefined
&& matches!(index, Value::Number(_))
&& get_root_var(refr)?.text() == "data"
{
let index = index.to_string();
v = obj[index].clone();
}
return Ok(Self::get_value_chained(v, &path[..]));
}
},
_ => {
path.reverse();
return Ok(Self::get_value_chained(self.eval_expr(expr)?, &path[..]));
}
}
}
}
fn eval_bool_expr(
&mut self,
op: &BoolOp,
lhs_expr: &ExprRef,
rhs_expr: &ExprRef,
) -> Result<Value> {
let lhs = self.eval_expr(lhs_expr)?;
let rhs = self.eval_expr(rhs_expr)?;
if lhs == Value::Undefined || rhs == Value::Undefined {
return Ok(Value::Undefined);
}
builtins::comparison::compare(op, &lhs, &rhs)
}
fn eval_bin_expr(&mut self, op: &BinOp, lhs: &ExprRef, rhs: &ExprRef) -> Result<Value> {
let lhs_value = self.eval_expr(lhs)?;
let rhs_value = self.eval_expr(rhs)?;
if lhs_value == Value::Undefined || rhs_value == Value::Undefined {
return Ok(Value::Undefined);
}
match op {
BinOp::Union => builtins::sets::union(lhs, rhs, lhs_value, rhs_value),
BinOp::Intersection => builtins::sets::intersection(lhs, rhs, lhs_value, rhs_value),
}
}
fn eval_arith_expr(
&mut self,
span: &Span,
op: &ArithOp,
lhs: &ExprRef,
rhs: &ExprRef,
) -> Result<Value> {
let lhs_value = self.eval_expr(lhs)?;
let rhs_value = self.eval_expr(rhs)?;
if lhs_value == Value::Undefined || rhs_value == Value::Undefined {
return Ok(Value::Undefined);
}
match (op, &lhs_value, &rhs_value) {
(ArithOp::Sub, Value::Set(_), _) | (ArithOp::Sub, _, Value::Set(_)) => {
builtins::sets::difference(lhs, rhs, lhs_value, rhs_value)
}
_ => builtins::numbers::arithmetic_operation(
span,
op,
lhs,
rhs,
lhs_value,
rhs_value,
self.compiled_policy.strict_builtin_errors,
),
}
}
fn eval_every(
&mut self,
_span: &Span,
key: &Option<Span>,
value: &Span,
domain: &ExprRef,
query: &Ref<Query>,
) -> Result<bool> {
self.check_execution_time()?;
let domain = self.eval_expr(domain)?;
self.scopes.push(Scope::new());
self.contexts.push(Context {
value: Value::new_set(),
..Context::default()
});
let mut r = true;
match domain {
Value::Array(a) => {
for (idx, v) in a.iter().enumerate() {
self.add_variable(&value.source_str(), v.clone())?;
if let Some(key) = key {
self.add_variable(&key.source_str(), Value::from(idx))?;
}
if !self.eval_query(query)? {
r = false;
break;
}
}
}
Value::Set(s) => {
for v in s.iter() {
self.add_variable(&value.source_str(), v.clone())?;
if let Some(key) = key {
self.add_variable(&key.source_str(), v.clone())?;
}
if !self.eval_query(query)? {
r = false;
break;
}
}
}
Value::Object(o) => {
for (k, v) in o.iter() {
self.add_variable(&value.source_str(), v.clone())?;
if let Some(key) = key {
self.add_variable(&key.source_str(), k.clone())?;
}
if !self.eval_query(query)? {
r = false;
break;
}
}
}
_ => {
r = false;
}
};
self.contexts.pop();
self.scopes.pop();
Ok(r)
}
/// Execute a destructuring plan against a value, binding variables as needed
pub fn execute_destructuring_plan(
&mut self,
plan: &DestructuringPlan,
value: &Value,
) -> Result<Value> {
self.check_execution_time()?;
if value == &Value::Undefined {
return Ok(Value::Undefined);
}
fn compare(v1: &Value, v2: &Value) -> Result<Value> {
if v1 != v2 || v1 == &Value::Undefined {
Ok(Value::Undefined)
} else {
Ok(Value::from(true))
}
}
match plan {
DestructuringPlan::Var(var_name) => {
// Bind the variable to the value
self.add_variable(&var_name.source_str(), value.clone())?;
Ok(Value::Bool(true))
}
DestructuringPlan::Ignore => Ok(Value::Bool(true)),
DestructuringPlan::EqualityExpr(expected_expr) => {
let expected = self.eval_expr(expected_expr)?;
compare(value, &expected)
}
DestructuringPlan::EqualityValue(expected) => Ok(Value::from(value == expected)),
DestructuringPlan::Array { element_plans } => {
// Value must be an array with matching length
if let Value::Array(arr) = value {
if arr.len() != element_plans.len() {
return Ok(Value::Undefined);
}
// Recursively execute each element plan
for (i, element_plan) in element_plans.iter().enumerate() {
let Some(element) = arr.get(i) else {
return Ok(Value::Undefined);
};
if self.execute_destructuring_plan(element_plan, element)?
!= Value::from(true)
{
return Ok(Value::Undefined);
}
}
Ok(Value::from(true))
} else {
Ok(Value::Undefined) // Not an array
}
}
DestructuringPlan::Object {
field_plans,
dynamic_fields,
} => {
// Value must be an object with matching fields
if let Value::Object(obj) = value {
// Check that all required fields are present and match
for (key, field_plan) in field_plans {
if let Some(field_value) = obj.get(key) {
if self.execute_destructuring_plan(field_plan, field_value)?
!= Value::from(true)
{
return Ok(Value::Undefined);
}
} else {
return Ok(Value::Undefined); // Required field missing
}
}
if !dynamic_fields.is_empty() {
for (key_expr, field_plan) in dynamic_fields {
let key_value = self.eval_expr(key_expr)?;
if key_value == Value::Undefined {
return Ok(Value::Undefined);
}
if let Some(field_value) = obj.get(&key_value) {
if self.execute_destructuring_plan(field_plan, field_value)?
!= Value::from(true)
{
return Ok(Value::Undefined);
}
} else {
return Ok(Value::Undefined);
}
}
}
Ok(Value::from(true))
} else {
Ok(Value::Undefined) // Not an object
}
}
}
}
fn execute_assignment_plan(&mut self, plan: &AssignmentPlan) -> Result<Value> {
self.check_execution_time()?;
match plan {
AssignmentPlan::ColonEquals {
lhs_expr: _,
rhs_expr,
lhs_plan,
} => {
// For :=, evaluate RHS and bind to LHS pattern
let rhs_value = self.eval_expr(rhs_expr)?;
self.execute_destructuring_plan(lhs_plan, &rhs_value)
}
AssignmentPlan::EqualsBindLeft {
lhs_expr: _,
rhs_expr,
lhs_plan,
} => {
// For = with LHS binding, evaluate RHS and bind to LHS pattern
let rhs_value = self.eval_expr(rhs_expr)?;
self.execute_destructuring_plan(lhs_plan, &rhs_value)
}
AssignmentPlan::EqualsBindRight {
lhs_expr,
rhs_expr: _,
rhs_plan,
} => {
// For = with RHS binding, evaluate LHS and bind to RHS pattern
let lhs_value = self.eval_expr(lhs_expr)?;
self.execute_destructuring_plan(rhs_plan, &lhs_value)
}
AssignmentPlan::EqualsBothSides {
lhs_expr: _,
rhs_expr: _,
element_pairs,
} => {
// For = with both sides having patterns, execute each flattened pair
for (value_expr, pattern_plan) in element_pairs {
let value = self.eval_expr(value_expr)?;
if self.execute_destructuring_plan(pattern_plan, &value)? != Value::from(true) {
return Ok(Value::Undefined);
}
}
Ok(Value::from(true))
}
AssignmentPlan::WildcardMatch {
lhs_expr,
rhs_expr,
wildcard_side,
} => match wildcard_side {
WildcardSide::Both => Ok(Value::Bool(true)),
WildcardSide::Lhs => {
let rhs_value = self.eval_expr(rhs_expr)?;
if rhs_value == Value::Undefined {
Ok(Value::Undefined)
} else {
Ok(Value::Bool(true))
}
}
WildcardSide::Rhs => {
let lhs_value = self.eval_expr(lhs_expr)?;
if lhs_value == Value::Undefined {
Ok(Value::Undefined)
} else {
Ok(Value::Bool(true))
}
}
},
AssignmentPlan::EqualityCheck { lhs_expr, rhs_expr } => {
let lhs_value = self.eval_expr(lhs_expr)?;
let rhs_value = self.eval_expr(rhs_expr)?;
if lhs_value == Value::Undefined || rhs_value == Value::Undefined {
return Ok(Value::Undefined);
}
if lhs_value == rhs_value {
Ok(Value::Bool(true))
} else {
Ok(Value::Undefined)
}
}
}
}
fn eval_some_in(
&mut self,
_span: &Span,
_key_expr: &Option<ExprRef>,
_value_expr: &ExprRef,
collection: &ExprRef,
stmts: &[&LiteralStmt],
) -> Result<bool> {
self.check_execution_time()?;
let scope_saved = self.current_scope()?.clone();
let mut count: usize = 0;
// Fetch the binding plan for this some..in expression
let module_idx = self.current_module_index;
let expr_idx = collection.as_ref().eidx();
let binding_plan = self
.compiled_policy
.loop_hoisting_table
.get_expr_binding_plan(module_idx, expr_idx)
.map_err(|err| anyhow!("loop hoisting table out of bounds: {err}"))?;
let Some(BindingPlan::SomeIn {
key_plan,
value_plan,
..
}) = binding_plan.cloned()
else {
bail!("internal error: missing binding plan for some..in expression");
};
match self.eval_expr(collection)? {
Value::Array(a) => {
for (idx, value) in a.iter().enumerate() {
*self.current_scope_mut()? = scope_saved.clone();
let mut success = if let Some(key_plan) = &key_plan {
self.execute_destructuring_plan(key_plan, &Value::from(idx))?
== Value::from(true)
} else {
true
};
// Execute value binding
success = success
&& self.execute_destructuring_plan(&value_plan, value)?
== Value::from(true);
if !success {
*self.current_scope_mut()? = scope_saved.clone();
continue;
}
let mut should_break = false;
if self.eval_stmts(stmts)? {
count = count.saturating_add(1);
if let Some(ctx) = self.contexts.last() {
if ctx.early_return {
should_break = true;
}
}
}
*self.current_scope_mut()? = scope_saved.clone();
if should_break {
break;
}
}
}
Value::Set(s) => {
for value in s.iter() {
*self.current_scope_mut()? = scope_saved.clone();
let mut success = if let Some(key_plan) = &key_plan {
self.execute_destructuring_plan(key_plan, value)? == Value::from(true)
} else {
true
};
// Execute value binding
success = success
&& self.execute_destructuring_plan(&value_plan, value)?
== Value::from(true);
if !success {
*self.current_scope_mut()? = scope_saved.clone();
continue;
}
let mut should_break = false;
if self.eval_stmts(stmts)? {
count = count.saturating_add(1);
if let Some(ctx) = self.contexts.last() {
if ctx.early_return {
should_break = true;
}
}
}
*self.current_scope_mut()? = scope_saved.clone();
if should_break {
break;
}
}
}
Value::Object(o) => {
for (key, value) in o.iter() {
*self.current_scope_mut()? = scope_saved.clone();
let mut success = if let Some(key_plan) = &key_plan {
self.execute_destructuring_plan(key_plan, key)? == Value::from(true)
} else {
true
};
// Execute value binding
success = success
&& self.execute_destructuring_plan(&value_plan, value)?
== Value::from(true);
if !success {
*self.current_scope_mut()? = scope_saved.clone();
continue;
}
let mut should_break = false;
if self.eval_stmts(stmts)? {
count = count.saturating_add(1);
if let Some(ctx) = self.contexts.last() {
if ctx.early_return {
should_break = true;
}
}
}
*self.current_scope_mut()? = scope_saved.clone();
if should_break {
break;
}
}
}
Value::Undefined => (),
v => {
bail!(collection.span().error(
format!("`some .. in collection` expects array/set/object. Got `{v}`").as_str()
))
}
}
Ok(count > 0)
}
fn make_expression_result(span: &Span, v: &Value) -> Expression {
Expression {
value: v.clone(),
text: span.text().to_string().into(),
location: Location {
row: span.line,
col: span.col,
},
}
}
fn eval_stmt_impl(&mut self, stmt: &LiteralStmt, stmts: &[&LiteralStmt]) -> Result<bool> {
self.memory_check()?;
self.check_execution_time()?;
Ok(match &stmt.literal {
Literal::Expr { span, expr, .. } => {
let value = match expr.as_ref() {
Expr::Call {
span: call_span,
fcn,
params,
..
} => self.eval_call(
call_span,
expr,
fcn,
params,
get_extra_arg(
expr,
Some(self.current_module_path.as_str()),
&self.compiled_policy.functions,
),
true,
)?,
_ => self.eval_expr(expr)?,
};
if let Some(ctx) = self.contexts.last_mut() {
if let Some(result) = &mut ctx.result {
if value != Value::Undefined {
result
.expressions
.push(Self::make_expression_result(span, &value));
} else {
result.bindings = Value::new_object();
result.expressions.clear();
}
}
}
if let Value::Bool(bool) = value {
bool
} else {
// panic!();
// TODO: confirm this
// For non-booleans, treat anything other than undefined as true
value != Value::Undefined
}
}
Literal::NotExpr { span, expr, .. } => {
let value = match expr.as_ref() {
// Extra parameter is allowed; but a return argument is not allowed.
Expr::Call {
span: call_span,
fcn,
params,
..
} => self.eval_call(
call_span,
expr,
fcn,
params,
get_extra_arg(
expr,
Some(self.current_module_path.as_str()),
&self.compiled_policy.functions,
),
false,
)?,
_ => self.eval_expr(expr)?,
};
if let Some(ctx) = self.contexts.last_mut() {
if let Some(result) = &mut ctx.result {
result
.expressions
.push(Self::make_expression_result(span, &Value::Bool(true)));
}
}
// https://github.com/open-policy-agent/opa/issues/1622#issuecomment-520547385
matches!(value, Value::Bool(false) | Value::Undefined)
}
Literal::SomeVars { span, vars, .. } => {
for var in vars {
let name = var.source_str();
if self.current_scope()?.get(&name).is_some() {
bail!("duplicated definition of local variable {}", name);
}
self.add_variable_or(&name)?;
}
if let Some(ctx) = self.contexts.last_mut() {
if let Some(result) = &mut ctx.result {
result
.expressions
.push(Self::make_expression_result(span, &Value::Bool(true)));
}
}
true
}
Literal::SomeIn {
span,
key,
value,
collection,
} => {
if let Some(ctx) = self.contexts.last_mut() {
if let Some(result) = &mut ctx.result {
result
.expressions
.push(Self::make_expression_result(span, &Value::Bool(true)));
}
}
self.eval_some_in(span, key, value, collection, stmts)?
}
Literal::Every {
span,
key,
value,
domain,
query,
} => {
if let Some(ctx) = self.contexts.last_mut() {
if let Some(result) = &mut ctx.result {
result
.expressions
.push(Self::make_expression_result(span, &Value::Bool(true)));
}
}
self.eval_every(span, key, value, domain, query)?
}
})
}
fn apply_with_modifiers(&mut self, stmt: &LiteralStmt) -> Result<(Option<State>, bool)> {
if !stmt.with_mods.is_empty() {
// Save state;
let with_document = self.with_document.clone();
let input = self.input.clone();
let data = self.data.clone();
let processed = self.processed.clone();
let with_functions = self.with_functions.clone();
let rule_values = self.rule_values.clone();
self.processed.clear();
let processed_paths =
core::mem::replace(&mut self.processed_paths, Value::new_object());
self.rule_values.clear();
let mut skip_exec = false;
// Apply with modifiers.
for wm in &stmt.with_mods {
let path = Parser::get_path_ref_components(&wm.refr)?;
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
let mut target = path.join(".");
let mut target_is_function = self.lookup_function_by_name(&target).is_some()
|| Self::is_builtin(wm.refr.span(), &target);
if !target_is_function
&& !target.starts_with("data.")
&& !target.starts_with("input.")
&& target != "input"
{
// target must be a function.
if self.lookup_function_by_name(&target).is_none()
&& !Self::is_builtin(wm.refr.span(), &target)
{
// Prefix target with current module path.
target = format!("{}.{}", self.current_module_path, target);
if self.lookup_function_by_name(&target).is_none() {
bail!(wm.refr.span().error("undefined rule"));
}
target_is_function = true;
}
}
if target_is_function {
match self.eval_expr(&wm.r#as) {
Ok(v) if v != Value::Undefined => {
// Function replaced by value.
self.with_functions
.insert(target, FunctionModifier::Value(v));
}
_ => {
// Function replaced by another function.
// Lookup by with current module path prefixed.
let mut function_path =
get_path_string(&wm.r#as, Some(&self.current_module_path))?;
if self.lookup_function_by_name(&function_path).is_none() {
// Lookup without current module path prefixed.
function_path = get_path_string(&wm.r#as, None)?;
if self.lookup_function_by_name(&function_path).is_none()
&& !Self::is_builtin(wm.r#as.span(), &function_path)
{
// bail!(wm.r#as.span().error("could not evaluate expression"));
skip_exec = true;
}
}
self.with_functions
.insert(target, FunctionModifier::Function(function_path));
}
}
} else {
let value = self.eval_expr(&wm.r#as)?;
skip_exec = value == Value::Undefined;
let Some(first) = path.first() else {
bail!(wm.refr.span().error("empty path in with modifier"));
};
if *first == "input" || *first == "data" {
// Override existing values in case of conflict.
let mut obj = &mut self.with_document;
for p in &path {
if !matches!(obj, Value::Object(_)) {
*obj = Value::new_object();
}
obj = obj
.as_object_mut()?
.entry(Value::String(p.to_string().into()))
.or_insert(Value::new_object());
}
*obj = value;
// Mark modified rules as processed.
if let Some(rules) = self.compiled_policy.rules.get(&target) {
for r in rules {
self.processed.insert(r.clone());
}
}
} else {
bail!(wm.refr.span().error("not a valid target for with modifier"));
}
}
}
self.data = self.with_document["data"].clone();
self.input = self.with_document["input"].clone();
Ok((
Some((
with_document,
input,
data,
processed,
processed_paths,
with_functions,
rule_values,
)),
skip_exec,
))
} else {
Ok((None, false))
}
}
fn restore_state(&mut self, saved_state: Option<State>) -> Result<()> {
if let Some(s) = saved_state {
(
self.with_document,
self.input,
self.data,
self.processed,
self.processed_paths,
self.with_functions,
self.rule_values,
) = s;
}
Ok(())
}
fn eval_stmt(&mut self, stmt: &LiteralStmt, stmts: &[&LiteralStmt]) -> Result<bool> {
let (saved_state, skip_exec) = self.apply_with_modifiers(stmt)?;
let r = if !skip_exec {
self.eval_stmt_impl(stmt, stmts)
} else {
Ok(false)
};
self.restore_state(saved_state)?;
r
}
fn clear_scope(scope: &mut Scope) {
// Set each value to undefined. This is equivalent to removing the key.
for (_, v) in scope.iter_mut() {
*v = Value::Undefined;
}
}
fn eval_stmts_in_loop(
&mut self,
stmts: &[&LiteralStmt],
loops: &[HoistedLoop],
) -> Result<bool> {
self.memory_check()?;
self.check_execution_time()?;
if loops.is_empty() {
if let Some((first_stmt, tail_stmts)) = stmts.split_first() {
// Evaluate the current statement whose loop expressions have been hoisted.
if self.eval_stmt(first_stmt, tail_stmts)? {
if !matches!(&first_stmt.literal, Literal::SomeIn { .. }) {
self.eval_stmts(tail_stmts)
} else {
Ok(true)
}
} else {
Ok(false)
}
} else {
self.eval_stmts(stmts)
}
} else {
let (loop_info, loop_tail) = loops
.split_first()
.ok_or_else(|| anyhow!("internal error: expected loop info"))?;
let (first_stmt, _tail_stmts) = stmts
.split_first()
.ok_or_else(|| anyhow!("internal error: expected statements for loop"))?;
let mut result = false;
// Apply with modifiers before evaluating the loop expression.
let (saved_state, _) = self.apply_with_modifiers(first_stmt)?;
let collection_expr = Self::loop_collection_expr(loop_info).clone();
let loop_value = if let Expr::Call {
span, fcn, params, ..
} = collection_expr.as_ref()
{
// Handle walk(obj, output_param)
let extra_arg = get_extra_arg(
&collection_expr,
Some(self.current_module_path.as_str()),
&self.compiled_policy.functions,
);
// If there is an extra arg, ignore it while computing the loop value.
let params_slice: &[ExprRef] = if extra_arg.is_some() {
let (_, prefix) = params
.split_last()
.ok_or_else(|| anyhow!("internal error: expected params"))?;
prefix
} else {
params.as_slice()
};
self.eval_call_impl(span, &collection_expr, fcn, params_slice)?
} else {
self.eval_expr(&collection_expr)?
};
// Restore with modifiers.
// TODO: Delay this restore so that the stmt doesn't have to apply with modifiers again.
self.restore_state(saved_state)?;
// If the loop's index variable h<as already been assigned a value
// (this can happen if the same index is used for two different collections),
// then evaluate statements only if the index applies to this collection.
if let Some(walk_plan) = self.get_walk_binding_plan(loop_info)? {
let loop_target_expr = Self::loop_assignment_expr(loop_info);
self.scopes.push(Scope::default());
let query_result = self.get_current_context()?.result.clone();
let mut walk_result = false;
match loop_value {
Value::Array(items) => {
for item in items.iter() {
self.memory_check()?;
self.set_loop_var_value(loop_target_expr, item.clone())?;
if self.execute_destructuring_plan(&walk_plan, item)?
== Value::from(true)
{
walk_result =
self.eval_stmts_in_loop(stmts, loop_tail)? || walk_result;
}
Self::clear_scope(self.current_scope_mut()?);
if let Some(ctx) = self.contexts.last_mut() {
ctx.result.clone_from(&query_result);
if ctx.early_return {
break;
}
}
}
}
Value::Undefined => (),
other => {
let span = Self::loop_span(loop_info);
bail!(span
.error(format!("walk expected array result, got `{other}`").as_str()));
}
}
self.scopes.pop();
self.remove_loop_var_value(loop_target_expr);
return Ok(walk_result);
}
let index_expr = Self::loop_index_expr(loop_info);
if let Some(Expr::Var {
span: index_var, ..
}) = index_expr.map(|r| r.as_ref())
{
if let Some(idx) = self.lookup_local_var(&index_var.source_str()) {
if loop_value[&idx] != Value::Undefined {
result = self.eval_stmts_in_loop(stmts, loop_tail)? || result;
return Ok(result);
} else if idx != Value::Undefined {
// The index is not valid for this collection.
return Ok(false);
}
}
}
let index_plan = if let Some(index) = index_expr {
let module_idx = self.current_module_index;
let expr_idx = index.as_ref().eidx();
let plan = self
.compiled_policy
.loop_hoisting_table
.get_expr_binding_plan(module_idx, expr_idx)
.map_err(|err| anyhow!("loop hoisting table out of bounds: {err}"))?;
match plan.cloned() {
Some(BindingPlan::LoopIndex {
destructuring_plan, ..
}) => destructuring_plan,
Some(other_plan) => {
bail!("internal error: expected LoopIndex for loop index expression, got {:?}", other_plan);
}
None => {
bail!("internal error: no binding plan found for loop index expression");
}
}
} else {
bail!("internal error: no binding plan found for loop index expression");
};
// Create a new scope.
self.scopes.push(Scope::default());
let query_result = self.get_current_context()?.result.clone();
let loop_target_expr = Self::loop_assignment_expr(loop_info);
match loop_value {
Value::Array(items) => {
for (idx, v) in items.iter().enumerate() {
self.memory_check()?;
self.set_loop_var_value(loop_target_expr, v.clone())?;
if self.execute_destructuring_plan(&index_plan, &Value::from(idx))?
== Value::from(true)
{
result = self.eval_stmts_in_loop(stmts, loop_tail)? || result;
}
Self::clear_scope(self.current_scope_mut()?);
if let Some(ctx) = self.contexts.last_mut() {
ctx.result.clone_from(&query_result);
if ctx.early_return {
break;
}
}
}
self.remove_loop_var_value(loop_target_expr);
}
Value::Set(items) => {
for v in items.iter() {
self.memory_check()?;
self.set_loop_var_value(loop_target_expr, v.clone())?;
// For sets, index is also the value.
if self.execute_destructuring_plan(&index_plan, v)? == Value::from(true) {
result = self.eval_stmts_in_loop(stmts, loop_tail)? || result;
}
Self::clear_scope(self.current_scope_mut()?);
if let Some(ctx) = self.contexts.last_mut() {
ctx.result.clone_from(&query_result);
if ctx.early_return {
break;
}
}
}
self.remove_loop_var_value(loop_target_expr);
}
Value::Object(obj) => {
for (k, v) in obj.iter() {
self.memory_check()?;
self.set_loop_var_value(loop_target_expr, v.clone())?;
// For objects, index is key.
if self.execute_destructuring_plan(&index_plan, k)? == Value::from(true) {
result = self.eval_stmts_in_loop(stmts, loop_tail)? || result;
}
Self::clear_scope(self.current_scope_mut()?);
if let Some(ctx) = self.contexts.last_mut() {
ctx.result.clone_from(&query_result);
if ctx.early_return {
break;
}
}
}
self.remove_loop_var_value(loop_target_expr);
}
Value::Undefined => {
result = false;
}
_ => {
// The item is not a collection.
result = false;
}
}
self.scopes.pop();
// Return true if at least on iteration returned true
Ok(result)
}
}
fn eval_rule_ref(&mut self, rule_refr: &ExprRef) -> Result<Vec<Value>> {
self.check_execution_time()?;
let mut comps = vec![];
let mut expr = rule_refr;
loop {
match expr.as_ref() {
Expr::Var { span: v, .. } => {
comps.push(Value::String(v.text().into()));
break;
}
Expr::RefBrack {
refr: nested_refr,
index,
..
} => {
comps.push(self.eval_expr(index)?);
expr = nested_refr;
}
Expr::RefDot {
refr: nested_refr,
field,
..
} => {
comps.push(Value::String(field.0.text().into()));
expr = nested_refr;
}
_ => {
bail!(expr.span().error("not a valid rule ref"));
}
}
}
comps.reverse();
Ok(comps)
}
fn update_rule_value(
&mut self,
span: &Span,
path: Vec<Value>,
mut value: Value,
is_set: bool,
) -> Result<()> {
// If rule's value already exists in initial document, prefer it.
{
let mut init_obj = &self.init_data;
for p in path.iter() {
init_obj = &init_obj[p];
}
if init_obj != &Value::Undefined {
return Ok(());
}
}
let mut obj = &mut self.data;
let len = path.len();
for (idx, p) in path.into_iter().enumerate() {
// Stop at the first undefined component in the path
if p == Value::Undefined {
break;
}
if idx == len.saturating_sub(1) {
// last key.
if is_set {
let set = obj
.as_object_mut()
.map_err(|_| anyhow!(span.error("previous value is not an object")))?
.entry(p)
.or_insert(Value::new_set())
.as_set_mut()
.map_err(|_| anyhow!(span.error("previous value is not a set")))?;
set.append(value.as_set_mut()?);
} else {
let obj = obj
.as_object_mut()
.map_err(|_| anyhow!(span.error("previous value is not an object")))?;
match obj.entry(p) {
BTreeMapEntry::Vacant(v) => {
if value != Value::Undefined {
v.insert(value);
} else {
// TODO: clean this assumption between Undefined vs Object.
v.insert(Value::new_object());
}
}
BTreeMapEntry::Occupied(o) => {
if o.get() != &value && value != Value::Undefined {
bail!(span
.error("complete rules should not produce multiple outputs"))
}
}
}
}
break;
} else {
obj = obj
.as_object_mut()
.map_err(|_| anyhow!(span.error("previous value is not an object")))?
.entry(p)
.or_insert(Value::new_object());
}
}
Ok(())
}
// A ref is a constant ref, if it does not contain any local variables.
// For now, we restrict constant refs to those that contain only simple literals.
fn is_constant_ref(mut expr: &Ref<Expr>) -> Result<bool> {
loop {
match expr.as_ref() {
Expr::Var { .. } => break,
Expr::RefDot { refr, .. } => expr = refr,
Expr::RefBrack { refr, index, .. } if Self::is_simple_literal(index)? => {
expr = refr;
}
_ => return Ok(false),
}
}
Ok(true)
}
fn is_simple_literal(expr: &Ref<Expr>) -> Result<bool> {
Ok(matches!(
expr.as_ref(),
Expr::String { .. }
| Expr::RawString { .. }
| Expr::Bool { .. }
| Expr::Null { .. }
| Expr::Number { .. }
))
}
// A rule's output expression is constant if it does not contain local variables.
// For now, we restrict output expressions to those that contain only simple literals.
fn is_constant_output(key_expr: &Option<Ref<Expr>>, output_expr: &Ref<Expr>) -> Result<bool> {
let is_const = if let Some(key_expr) = key_expr {
Self::is_simple_literal(key_expr)?
} else {
true
};
Ok(is_const && Self::is_simple_literal(output_expr)?)
}
fn eval_output_expr_in_loop(&mut self, loops: &[HoistedLoop]) -> Result<bool> {
self.check_execution_time()?;
if loops.is_empty() {
let (key_expr, output_expr) = self.get_exprs_from_context()?;
let ctx = self.get_current_context()?;
let (is_set, is_old_style_set, is_rule, constness_determined) = (
ctx.is_set,
ctx.is_old_style_set,
!ctx.is_compr,
ctx.output_constness_determined,
);
if let Some(rule_ref) = ctx.rule_ref.clone() {
let mut is_const_rule = if is_rule && !constness_determined {
Self::is_constant_ref(&rule_ref)?
} else {
// Constness has already been determined or is not a rule.
// Treat the expression as not constant.
false
};
let mut comps = self.eval_rule_ref(&rule_ref)?;
if let Some(ke) = &key_expr {
comps.push(self.eval_expr(ke)?);
}
let output = if let Some(oe) = &output_expr {
// Rule is constant only if its ref, key and output are constant.
is_const_rule = is_const_rule && Self::is_constant_output(&key_expr, oe)?;
self.eval_expr(oe)?
} else if is_old_style_set && !comps.is_empty() {
// Rule's constness is determined only by its ref.
let output = comps
.last()
.cloned()
.ok_or_else(|| anyhow!("internal error: missing comps output"))?;
comps.pop();
output
} else {
// Rule's constness is determined only by its ref.
Value::Bool(true)
};
let comps_defined = comps.iter().all(|v| v != &Value::Undefined);
let ctx_mut = self.get_current_context_mut()?;
if is_const_rule {
ctx_mut.early_return = true;
}
if is_rule {
ctx_mut.output_constness_determined = true;
}
if output == Value::Undefined || !comps_defined {
ctx_mut.rule_value = Value::Undefined;
return Ok(false);
}
if is_set {
// Ensure that set rule is created even if the element is undefined.
let set = ctx_mut
.rule_value
.as_object_mut()?
.entry(Value::from_array(comps))
.or_insert(Value::new_set());
if output != Value::Undefined {
set.as_set_mut()?.insert(output);
return Ok(true);
}
return Ok(false);
}
// Non-set rule.
match ctx_mut
.rule_value
.as_object_mut()?
.entry(Value::from_array(comps))
{
BTreeMapEntry::Vacant(v) => {
v.insert(output);
}
BTreeMapEntry::Occupied(o) if o.get() != &output => bail!(rule_ref
.span()
.error("rules must not produce multiple outputs")),
_ => {
// Rule produced same value.
}
}
return Ok(true);
}
match (key_expr, output_expr) {
(Some(ke), Some(oe)) => {
let key = self.eval_expr(&ke)?;
let value = self.eval_expr(&oe)?;
let ctx_mut = self.get_current_context_mut()?;
if key != Value::Undefined && value != Value::Undefined {
let map = ctx_mut.value.as_object_mut()?;
match map.get(&key) {
Some(pv) if *pv != value => {
let span = ke.span();
return Err(span.source.error(
span.line,
span.col,
format!(
"value for key `{}` generated multiple times: `{}` and `{}`",
serde_json::to_string_pretty(&key).map_err(anyhow::Error::msg)?,
serde_json::to_string_pretty(&pv).map_err(anyhow::Error::msg)?,
serde_json::to_string_pretty(&value).map_err(anyhow::Error::msg)?,
)
.as_str(),
));
}
_ => map.insert(key, value),
};
} else {
match &ctx_mut.value {
Value::Object(_) => (),
_ => ctx_mut.value = Value::Undefined,
}
};
}
(None, Some(oe)) => {
let output = self.eval_expr(&oe)?;
let ctx_mut = self.get_current_context_mut()?;
if output != Value::Undefined {
match &mut ctx_mut.value {
Value::Array(a) => {
Rc::make_mut(a).push(output);
}
Value::Set(ref mut s) => {
Rc::make_mut(s).insert(output);
}
a => bail!("internal error: invalid context value {a}"),
}
} else if !ctx_mut.is_compr {
match &ctx_mut.value {
Value::Set(_) => (),
_ => ctx_mut.value = Value::Undefined,
}
}
}
// No output expression.
// TODO: should we just push a Bool(true)?
_ => (),
}
// If a query snippet is being run, gather results.
let result_opt = {
let current_ctx = self.get_current_context_mut()?;
current_ctx.result.clone()
};
if let Some(mut current_result) = result_opt {
if let Some(scope) = self.scopes.last() {
for (name, value) in scope.iter() {
current_result
.bindings
.as_object_mut()?
.insert(Value::String(name.to_string().into()), value.clone());
}
}
if current_result.expressions.len() == 1 // Single expression query
|| current_result // Multi expression query where no value is false
.expressions
.iter()
.all(|v| v.value != Value::Undefined && v.value != Value::Bool(false))
&& !current_result.expressions.is_empty()
{
let ctx_mut = self.get_current_context_mut()?;
ctx_mut.results.result.push(current_result);
}
}
return Ok(true);
}
// Try out values in current loop expr.
let (loop_info, loop_tail) = loops
.split_first()
.ok_or_else(|| anyhow!("internal error: expected loop info"))?;
let mut result = false;
let loop_target_expr = Self::loop_assignment_expr(loop_info);
match self.eval_expr(Self::loop_collection_expr(loop_info))? {
Value::Array(items) => {
for v in items.iter() {
self.set_loop_var_value(loop_target_expr, v.clone())?;
result = self.eval_output_expr_in_loop(loop_tail)? || result;
}
}
Value::Set(items) => {
for v in items.iter() {
self.set_loop_var_value(loop_target_expr, v.clone())?;
result = self.eval_output_expr_in_loop(loop_tail)? || result;
}
}
Value::Object(obj) => {
for (_, v) in obj.iter() {
self.set_loop_var_value(loop_target_expr, v.clone())?;
result = self.eval_output_expr_in_loop(loop_tail)? || result;
}
}
_ => {
let span = Self::loop_span(loop_info);
return Err(span
.source
.error(span.line, span.col, "item cannot be indexed"));
}
}
self.remove_loop_var_value(loop_target_expr);
Ok(result)
}
fn get_current_context(&self) -> Result<&Context> {
match self.contexts.last() {
Some(ctx) => Ok(ctx),
_ => bail!("internal error: no active context found"),
}
}
fn get_current_context_mut(&mut self) -> Result<&mut Context> {
match self.contexts.last_mut() {
Some(ctx) => Ok(ctx),
_ => bail!("internal error: no active context found"),
}
}
fn get_exprs_from_context(&self) -> Result<ContextExprs> {
let ctx = self.get_current_context()?;
Ok((ctx.key_expr.clone(), ctx.output_expr.clone()))
}
fn eval_output_expr(&mut self) -> Result<bool> {
self.check_execution_time()?;
// Evaluate output expression after all the statements have been executed.
let (key_expr, output_expr) = self.get_exprs_from_context()?;
let mut loops: Vec<HoistedLoop> = Vec::new();
// Get pre-computed loops for key expression
if let Some(ke) = &key_expr {
match self
.compiled_policy
.loop_hoisting_table
.get_expr_loops(self.current_module_index, ke.as_ref().eidx())
.map_err(|err| anyhow!("loop hoisting table out of bounds: {err}"))?
{
Some(hoisted_loops) => {
loops.extend(hoisted_loops.clone());
}
None => {
bail!(ke.span().error("Loop hoisting information not found for key expression. This is likely a bug in the compilation phase."));
}
}
}
// Get pre-computed loops for output expression
if let Some(oe) = &output_expr {
match self
.compiled_policy
.loop_hoisting_table
.get_expr_loops(self.current_module_index, oe.as_ref().eidx())
.map_err(|err| anyhow!("loop hoisting table out of bounds: {err}"))?
{
Some(hoisted_loops) => {
loops.extend(hoisted_loops.clone());
}
None => {
bail!(oe.span().error("Loop hoisting information not found for output expression. This is likely a bug in the compilation phase."));
}
}
}
let r = self.eval_output_expr_in_loop(&loops[..])?;
let ctx = self.get_current_context()?;
ctx.output_expr
.as_ref()
.map_or_else(|| Ok(r), |_oe| Ok(ctx.rule_value != Value::Undefined))
}
fn eval_stmts(&mut self, stmts: &[&LiteralStmt]) -> Result<bool> {
let mut eval_success = true;
for (idx, stmt) in stmts.iter().enumerate() {
self.memory_check()?;
if !eval_success {
break;
}
// Get pre-computed hoisted loops from compilation phase
let loop_exprs: Vec<HoistedLoop> = match self
.compiled_policy
.loop_hoisting_table
.get_statement_loops(self.current_module_index, stmt.sidx)
.map_err(|err| anyhow!("loop hoisting table out of bounds: {err}"))?
{
Some(hoisted_loops) => {
// Use pre-computed loops from compilation phase
hoisted_loops.clone()
}
None => {
// Loop hoisting should have been done during compilation
// If we reach here, it means the hoisting pass didn't process this statement
bail!(stmt.span.error("Loop hoisting information not found for statement. This is likely a bug in the compilation phase."));
}
};
if !loop_exprs.is_empty() {
// If there are hoisted loop expressions, execute subsequent statements
// within loops.
let remaining = stmts.get(idx..).unwrap_or(&[]);
return self.eval_stmts_in_loop(remaining, &loop_exprs[..]);
}
let tail = idx
.checked_add(1)
.and_then(|n| stmts.get(n..))
.unwrap_or(&[]);
eval_success = self.eval_stmt(stmt, tail)?;
if matches!(&stmt.literal, Literal::SomeIn { .. }) {
return Ok(eval_success);
}
}
if eval_success {
eval_success = self.eval_output_expr()?;
} else {
// If a query snippet is being run, gather results.
let result_opt = {
let ctx = self.get_current_context_mut()?;
ctx.result.clone()
};
if let Some(mut gathered_result) = result_opt {
if let Some(scope) = self.scopes.last() {
for (name, value) in scope.iter() {
gathered_result
.bindings
.as_object_mut()?
.insert(Value::String(name.to_string().into()), value.clone());
}
}
if gathered_result.expressions.len() == 1 // Single expression query
|| gathered_result // Multi expression query where no value is false
.expressions
.iter()
.all(|v| v.value != Value::Undefined && v.value != Value::Bool(false))
&& !gathered_result.expressions.is_empty()
{
let ctx = self.get_current_context_mut()?;
ctx.results.result.push(gathered_result);
}
}
}
Ok(eval_success)
}
fn eval_query(&mut self, query: &Ref<Query>) -> Result<bool> {
self.check_execution_time()?;
// Execute the query in a new scope
self.scopes.push(Scope::new());
let order_indices = {
let query_module_index = u32::try_from(self.compiled_policy.modules.len())
.map_err(|err| anyhow!("query module index overflow: {err}"))?;
if self.current_module_index == query_module_index {
// Use query schedule for the current module
let schedule = match self.query_schedule.as_ref() {
Some(s) => s
.queries
.get_checked(query_module_index, query.qidx)
.map_err(|err| anyhow!("schedule out of bounds: {err}"))?,
None => None,
};
match schedule {
Some(schedule) => Some(&schedule.order),
None => {
if self.query_schedule.is_some() {
bail!(query
.span
.error("statements not scheduled in query {query:?}"));
}
None
}
}
} else {
// Use compiled policy schedule for other modules
let schedule = match self.compiled_policy.schedule.as_ref() {
Some(s) => s
.queries
.get_checked(self.current_module_index, query.qidx)
.map_err(|err| anyhow!("schedule out of bounds: {err}"))?,
None => None,
};
match schedule {
Some(schedule) => Some(&schedule.order),
None => {
if self.compiled_policy.schedule.is_some() {
bail!(query
.span
.error("statements not scheduled in query {query:?}"));
}
None
}
}
}
};
let ordered_stmts: Vec<&LiteralStmt> = match order_indices {
Some(order) => {
let stmts_len = query.stmts.len();
if order.len() != stmts_len {
let msg = format!(
"invalid schedule: expected {stmts_len} statement indices, found {}",
order.len()
);
bail!(query.span.error(msg.as_str()));
}
let mut ordered = Vec::with_capacity(stmts_len);
for idx in order {
let stmt_idx = usize::from(*idx);
let stmt = query.stmts.get(stmt_idx).ok_or_else(|| {
let msg = format!(
"invalid schedule index {stmt_idx} for {} statements",
stmts_len
);
query.span.error(msg.as_str())
})?;
ordered.push(stmt);
}
ordered
}
None => query.stmts.iter().collect(),
};
let r = self.eval_stmts(&ordered_stmts);
self.scopes.pop();
r
}
fn eval_array(&mut self, items: &Vec<ExprRef>) -> Result<Value> {
self.check_execution_time()?;
let mut array = Vec::new();
for item in items {
let term = self.eval_expr(item)?;
if term == Value::Undefined {
return Ok(Value::Undefined);
}
array.push(term);
}
Ok(Value::from_array(array))
}
fn eval_object(&mut self, fields: &Vec<(Span, ExprRef, ExprRef)>) -> Result<Value> {
self.check_execution_time()?;
let mut object = BTreeMap::new();
for (_, key, value) in fields {
// TODO: check this
// While the grammar defines a object-item as
// ( scalar | ref | var ) ":" term, the OPA
// implementation is more like expr ":" expr
let key = self.eval_expr(key)?;
if key == Value::Undefined {
return Ok(Value::Undefined);
}
let value = self.eval_expr(value)?;
if value == Value::Undefined {
return Ok(Value::Undefined);
}
object.insert(key, value);
}
Ok(Value::from_map(object))
}
fn eval_set(&mut self, items: &Vec<ExprRef>) -> Result<Value> {
self.check_execution_time()?;
let mut set = BTreeSet::new();
for item in items {
let term = self.eval_expr(item)?;
if term == Value::Undefined {
return Ok(Value::Undefined);
}
set.insert(term);
}
Ok(Value::from_set(set))
}
fn eval_membership(
&mut self,
key: &Option<ExprRef>,
value: &ExprRef,
collection: &ExprRef,
) -> Result<Value> {
self.check_execution_time()?;
let value = self.eval_expr(value)?;
let collection = self.eval_expr(collection)?;
let result = match &collection {
Value::Array(array) => {
if let Some(key) = key {
let key = self.eval_expr(key)?;
collection[&key] == value
} else {
array.contains(&value)
}
}
Value::Object(object) => {
if let Some(key) = key {
let key = self.eval_expr(key)?;
collection[&key] == value
} else {
object.values().any(|item| *item == value)
}
}
Value::Set(set) => {
if key.is_some() {
false
} else {
set.contains(&value)
}
}
_ => {
false
//bail!(collection_expr.span().error("collection must be array, object or set"));
}
};
Ok(Value::Bool(result))
}
fn eval_array_compr(&mut self, term: &ExprRef, query: &Ref<Query>) -> Result<Value> {
self.check_execution_time()?;
// Push new context
self.contexts.push(Context {
output_expr: Some(term.clone()),
value: Value::new_array(),
is_compr: true,
..Context::default()
});
// Evaluate body first.
self.eval_query(query)?;
match self.contexts.pop() {
Some(ctx) => Ok(ctx.value),
None => bail!("internal error: context already popped"),
}
}
fn eval_set_compr(&mut self, term: &ExprRef, query: &Ref<Query>) -> Result<Value> {
self.check_execution_time()?;
// Push new context
self.contexts.push(Context {
output_expr: Some(term.clone()),
value: Value::new_set(),
is_compr: true,
..Context::default()
});
self.eval_query(query)?;
match self.contexts.pop() {
Some(ctx) => Ok(ctx.value),
None => bail!("internal error: context already popped"),
}
}
fn eval_object_compr(
&mut self,
key: &ExprRef,
value: &ExprRef,
query: &Ref<Query>,
) -> Result<Value> {
self.check_execution_time()?;
// Push new context
self.contexts.push(Context {
key_expr: Some(key.clone()),
output_expr: Some(value.clone()),
value: Value::new_object(),
is_compr: true,
..Context::default()
});
self.eval_query(query)?;
match self.contexts.pop() {
Some(ctx) => Ok(ctx.value),
None => bail!("internal error: context already popped"),
}
}
fn lookup_function_by_name(&self, path: &str) -> Option<(&Vec<Ref<Rule>>, &Ref<Module>)> {
let mut path = path.to_owned();
if !path.starts_with("data.") {
path = format!("{}.{}", self.current_module_path, path);
}
match self.compiled_policy.functions.get(&path) {
Some((f, _, m)) => Some((f, m)),
_ => None,
}
}
fn eval_builtin_call(
&mut self,
span: &Span,
name: &str,
builtin: builtins::BuiltinFcn,
params: &[ExprRef],
args: Vec<Value>,
) -> Result<Value> {
self.check_execution_time()?;
// If any argument is undefined, then the call is undefined.
if args.iter().any(|a| a == &Value::Undefined) {
return Ok(Value::Undefined);
}
let cache = builtins::must_cache(name);
if let Some(cached_key) = &cache {
if let Some(v) = self.builtins_cache.get(&(cached_key, args.clone())) {
return Ok(v.clone());
}
}
let v = match builtin.0(
span,
params,
&args[..],
self.compiled_policy.strict_builtin_errors,
) {
Ok(v) => v,
// Ignore errors if we are not evaluating in strict mode.
Err(_) if !self.compiled_policy.strict_builtin_errors => return Ok(Value::Undefined),
Err(e) => Err(e)?,
};
// Handle trace function.
// TODO: with modifier.
if name == "trace" {
if let (Some(traces), Value::String(msg)) = (&mut self.traces, &v) {
traces.push(msg.clone());
self.memory_check()?;
return Ok(Value::Bool(true));
}
}
if let Some(cached_key) = cache {
self.builtins_cache.insert((cached_key, args), v.clone());
}
self.memory_check()?;
Ok(v)
}
#[allow(unused_variables)]
fn lookup_builtin(span: &Span, path: &str) -> Result<Option<&'static BuiltinFcn>> {
if let Some(builtin) = builtins::BUILTINS.get(path) {
return Ok(Some(builtin));
}
Ok(None)
}
fn is_builtin(span: &Span, path: &str) -> bool {
path == "print" || matches!(Self::lookup_builtin(span, path), Ok(Some(_)))
}
fn to_printable(value: &Value, s: &mut String) {
match value {
Value::Array(array) => {
s.push('[');
for (idx, e) in array.iter().enumerate() {
if idx > 0 {
s.push_str(", ");
}
Self::to_printable(e, s);
}
s.push(']');
}
Value::Set(set) => {
s.push('{');
for (idx, e) in set.iter().enumerate() {
if idx > 0 {
s.push_str(", ");
}
Self::to_printable(e, s);
}
s.push('}');
}
Value::Object(map) => {
s.push('{');
for (idx, (k, entry_value)) in map.iter().enumerate() {
if idx > 0 {
s.push_str(", ");
}
Self::to_printable(k, s);
s.push_str(": ");
Self::to_printable(entry_value, s);
}
s.push('}');
}
other => s.push_str(&format!("{other}")),
}
}
fn eval_print(&mut self, span: &Span, params: &[ExprRef], args: Vec<Value>) -> Result<Value> {
const MAX_ARGS: u8 = 100;
if args.len() > usize::from(MAX_ARGS) {
bail!(span.error(&format!("print supports upto {MAX_ARGS} arguments")));
}
// If not compiling for std target, return early if gathering is not
// requested.
#[cfg(not(feature = "std"))]
if !self.gather_prints {
return Ok(Value::Bool(true));
}
let mut msg = String::default();
for (i, p) in params.iter().enumerate() {
if i > 0 {
msg.push(' ');
}
match self.eval_expr(p)? {
Value::Undefined => msg.push_str("<undefined>"),
// Do not print quotes for string values.
Value::String(s) => msg.push_str(&format!("{s}")),
a => Self::to_printable(&a, &mut msg),
}
}
if self.gather_prints {
// Prefix location information.
self.prints
.push(format!("{}:{}: {msg}", span.source.file(), span.line));
}
// Print to stderr only if not gathering.
#[cfg(feature = "std")]
if !self.gather_prints {
std::eprintln!("{msg}");
}
Ok(Value::Bool(true))
}
fn eval_call_impl(
&mut self,
span: &Span,
expr: &ExprRef,
fcn: &ExprRef,
params: &[ExprRef],
) -> Result<Value> {
self.check_execution_time()?;
// Return generated values of walk builtin.
if let Some(v) = self.get_loop_var_value(expr)? {
return Ok(v.clone());
}
let fcn_path = match get_path_string(fcn, None) {
Ok(p) => p,
_ => bail!(span.error("invalid function expression")),
};
let mut param_values = Vec::with_capacity(params.len());
for p in params {
param_values.push(self.eval_expr(p)?);
}
let orig_fcn_path = fcn_path.clone();
let mut with_functions_saved = None;
let selected_fcn_path = match self.with_functions.get(&orig_fcn_path) {
Some(FunctionModifier::Function(p)) => {
let p = p.clone();
with_functions_saved = Some(self.with_functions.clone());
self.with_functions.clear();
p
}
Some(FunctionModifier::Value(value_override)) => {
if param_values
.iter()
.any(|evaluated_value| evaluated_value == &Value::Undefined)
{
return Ok(Value::Undefined);
}
return Ok(value_override.clone());
}
_ => orig_fcn_path.clone(),
};
let mut extension = None;
let empty: Vec<Ref<Rule>> = vec![];
let (fcns_rules, fcn_module) = match self.lookup_function_by_name(&selected_fcn_path) {
Some((fcns, m)) => (fcns, Some(m.clone())),
_ => {
if self
.compiled_policy
.default_rules
.contains_key(&selected_fcn_path)
|| self
.compiled_policy
.default_rules
.contains_key(&get_path_string(fcn, Some(&self.current_module_path))?)
{
// process default functions later.
(&empty, self.module.clone())
}
// Look up extension.
else if let Some(ext) = self.extensions.get_mut(&selected_fcn_path) {
extension = Some(ext);
(&empty, None)
} else if selected_fcn_path == "print" {
return self.eval_print(span, params, param_values);
}
// Look up builtin function.
else if let Some(builtin) = Self::lookup_builtin(span, &selected_fcn_path)? {
let r = self.eval_builtin_call(
span,
&selected_fcn_path.clone(),
*builtin,
params,
param_values,
);
if let Some(with_functions) = with_functions_saved {
self.with_functions = with_functions;
}
return r;
} else {
bail!(
span.error(format!("could not find function {selected_fcn_path}").as_str())
);
}
}
};
if param_values.iter().any(|v| v == &Value::Undefined) {
if let Some(with_functions) = with_functions_saved {
self.with_functions = with_functions;
}
return Ok(Value::Undefined);
}
if let Some((nargs, ext)) = extension {
if param_values.len() != usize::from(*nargs) {
bail!(span.error("incorrect number of parameters supplied to extension"));
}
let r = Rc::make_mut(ext)(param_values);
// Restore with_functions.
if let Some(with_functions) = with_functions_saved {
self.with_functions = with_functions;
}
match r {
Ok(v) => return Ok(v),
Err(e) => bail!(span.error(&format!("{e}"))),
}
}
let fcns = fcns_rules.clone();
let mut results: Vec<Value> = Vec::new();
let mut errors: Vec<anyhow::Error> = Vec::new();
'outer: for fcn_rule in fcns {
let (args, output_expr, bodies) = match fcn_rule.as_ref() {
Rule::Spec {
head: RuleHead::Func { args, assign, .. },
bodies,
..
} => (args, assign.as_ref().map(|a| a.value.clone()), bodies),
_ => bail!("internal error not a function"),
};
if args.len() != params.len() {
return Err(span.source.error(
span.line,
span.col,
format!(
"mismatch in number of arguments. supplied {}, expected {}",
params.len(),
args.len()
)
.as_str(),
));
}
// Back up local variables of current function and empty
// the local variables of callee function.
let scopes = core::mem::take(&mut self.scopes);
// Set the arguments scope.
let args_scope = Scope::new();
self.scopes.push(args_scope);
// Determine the module index for the callee so we can fetch binding plans
let callee_module_idx = fcn_module
.as_ref()
.map(|module| self.find_module_index(module))
.unwrap_or(self.current_module_index);
for (idx, a) in args.iter().enumerate() {
// Fetch the binding plan for this function parameter
let module_idx = callee_module_idx;
let expr_idx = a.as_ref().eidx();
let param_value = param_values
.get(idx)
.ok_or_else(|| anyhow!("internal error: missing param value"))?;
let binding_success = if let Some(BindingPlan::Parameter {
destructuring_plan,
..
}) = self
.compiled_policy
.loop_hoisting_table
.get_expr_binding_plan(module_idx, expr_idx)
.map_err(|err| anyhow!("loop hoisting table out of bounds: {err}"))?
.cloned()
{
// Execute the destructuring plan with the parameter value
self.execute_destructuring_plan(&destructuring_plan, param_value)?
== Value::from(true)
} else {
// Raise error if binding plan is not found
return Err(span.error(&format!(
"binding plan not found for parameter {}",
a.span().text()
)));
};
if !binding_success {
self.scopes = scopes;
continue 'outer;
}
}
let ctx = Context {
output_expr: output_expr.clone(),
value: Value::new_set(),
..Context::default()
};
let prev_module = self.set_current_module(fcn_module.clone())?;
let value = match self.eval_rule_bodies(ctx, span, bodies) {
Ok(v) => v,
Err(e) => {
// If the rule produces an error, save the error.
errors.push(e);
self.scopes = scopes;
continue;
}
};
self.set_current_module(prev_module)?;
let result = match &value {
Value::Set(s) if s.len() == 1 => {
let Some(v) = s.iter().next() else {
return Err(anyhow!("internal error: expected single value in set"));
};
v.clone()
}
Value::Set(s) if !s.is_empty() => {
return Err(span.source.error(
span.line,
span.col,
format!("function produced multiple outputs {value:?}").as_str(),
))
}
// If the function successfully executed, but did not return any value, then return true.
Value::Set(s) if s.is_empty() && output_expr.is_none() => Value::Bool(true),
Value::Set(s) if s.is_empty() => Value::Undefined,
// If the function execution resulted in undefined, then propagate it.
Value::Undefined => Value::Undefined,
// Function returned a non set value
v => v.clone(),
};
// Restore local variables for current context.
self.scopes = scopes;
if result != Value::Undefined {
results.push(result);
}
}
if self.compiled_policy.strict_builtin_errors && !errors.is_empty() {
return Err(errors
.first()
.map(|e| anyhow!(e.to_string()))
.unwrap_or_else(|| anyhow!("internal error: missing error entry")));
}
if results.is_empty() {
// Back up local variables of current function and empty
// the local variables of callee function.
let scopes = core::mem::take(&mut self.scopes);
if errors.is_empty() {
// Check if any default rules can be evaluated.
// TODO: with mod
let rules = match self
.compiled_policy
.default_rules
.get(&selected_fcn_path)
.cloned()
{
Some(rules) => Some(rules),
None => {
let alt_fcn_path = get_path_string(fcn, Some(&self.current_module_path))?;
self.compiled_policy
.default_rules
.get(&alt_fcn_path)
.cloned()
}
};
if let Some(rules) = rules {
for (rule, _) in rules.iter() {
if let Rule::Default { value, .. } = rule.as_ref() {
match self.eval_expr(value) {
Ok(v) => results.push(v),
Err(e) => errors.push(e),
}
}
}
}
}
self.scopes = scopes;
}
if let Some(with_functions) = with_functions_saved {
self.with_functions = with_functions;
}
if results.is_empty() {
if errors.is_empty() {
return Ok(Value::Undefined);
} else {
return Err(errors
.first()
.map(|e| anyhow!(e.to_string()))
.unwrap_or_else(|| anyhow!("internal error: missing error entry")));
}
}
// all defined values should be the equal to the same value that should be returned
if results.windows(2).any(|w| matches!(w, [a, b] if a != b)) {
return Err(span.source.error(
span.line,
span.col,
"functions must not produce multiple outputs for same inputs",
));
}
results
.first()
.cloned()
.ok_or_else(|| anyhow!("internal error: expected function result"))
}
fn eval_call(
&mut self,
span: &Span,
expr: &ExprRef,
fcn: &ExprRef,
params: &[ExprRef],
extra_arg: Option<ExprRef>,
allow_return_arg: bool,
) -> Result<Value> {
self.check_execution_time()?;
// TODO: global var check; interop with `some var`
if extra_arg.is_some() {
let (last_param, arg_prefix) = params
.split_last()
.ok_or_else(|| anyhow!("internal error: expected at least one param"))?;
let value = self.eval_call_impl(span, expr, fcn, arg_prefix)?;
if allow_return_arg {
let module_idx = self.current_module_index;
let expr_idx = last_param.as_ref().eidx();
if let Some(BindingPlan::Parameter {
destructuring_plan, ..
}) = self
.compiled_policy
.loop_hoisting_table
.get_expr_binding_plan(module_idx, expr_idx)
.map_err(|err| anyhow!("loop hoisting table out of bounds: {err}"))?
.cloned()
{
// Execute the destructuring plan with the return value
let result = self.execute_destructuring_plan(&destructuring_plan, &value)?;
Ok(result)
} else {
// Raise error if binding plan is not found
Err(span.error(&format!(
"binding plan not found for parameter {}",
last_param.span().text()
)))
}
} else {
let expected = self.eval_expr(last_param)?;
Ok(Value::Bool(value == expected))
}
} else {
self.eval_call_impl(span, expr, fcn, params)
}
}
fn lookup_local_var(&self, name: &SourceStr) -> Option<Value> {
// Lookup local variables and arguments.
for scope in self.scopes.iter().rev() {
if let Some(v) = scope.get(name) {
return Some(v.clone());
}
}
None
}
fn ensure_module_evaluated(&mut self, path: String) -> Result<()> {
self.check_execution_time()?;
for module in self.compiled_policy.modules.clone().iter().cloned() {
if Some(&module) == self.module.as_ref() {
// Prevent cyclic evaluation.
continue;
}
let module_path_str = get_path_string(&module.package.refr, Some("data"))?;
let has_dot_after_prefix = module_path_str
.get(path.len()..)
.is_some_and(|suffix| suffix.starts_with('.'));
if module_path_str.starts_with(&path)
&& (module_path_str.len() == path.len() || has_dot_after_prefix)
{
// Ensure that the module is created.
let module_path_components = Parser::get_path_ref_components(&module.package.refr)?;
let module_path_components: Vec<&str> =
module_path_components.iter().map(|s| s.text()).collect();
let vref = Self::make_or_get_value_mut(&mut self.data, &module_path_components)?;
if *vref == Value::Undefined {
*vref = Value::new_object();
}
for rule in &module.policy {
if !self.processed.contains(rule) {
self.eval_rule(&module, rule)?;
}
}
let prev_module = self.set_current_module(Some(module.clone()))?;
for rule in &module.policy {
if !self.processed.contains(rule) {
self.eval_default_rule(rule)?;
}
}
self.set_current_module(prev_module)?;
self.mark_processed(&module_path_components)?;
}
}
Ok(())
}
fn ensure_rule_evaluated(&mut self, path: String) -> Result<()> {
self.check_execution_time()?;
let mut matched = false;
if let Some(rules) = self.compiled_policy.rules.get(&path) {
matched = true;
for r in rules.clone() {
if !self.processed.contains(&r) {
let module = self.get_rule_module(&r)?;
self.eval_rule(&module, &r)?;
}
}
}
// Evaluate the associated default rules after non-default rules
if let Some(rules) = self.compiled_policy.default_rules.get(&path) {
matched = true;
for (r, _) in rules.clone() {
if !self.processed.contains(&r) {
let module = self.get_rule_module(&r)?;
let prev_module = self.set_current_module(Some(module))?;
self.eval_default_rule(&r)?;
self.set_current_module(prev_module)?;
}
}
}
if matched {
let comps: Vec<&str> = path.split('.').collect();
if let Some((_, tail)) = comps.split_first() {
self.mark_processed(tail)?;
}
}
Ok(())
}
fn is_processed(&self, path: &[&str]) -> Result<bool> {
let mut obj = &self.processed_paths;
for p in path {
// Prefix has already been processed.
if obj[&Value::Undefined] == Value::Null {
return Ok(true);
}
match &obj[*p] {
// Prefix and its suffixes including path have not been processed.
Value::Undefined => return Ok(false),
v => obj = v,
}
}
Ok(obj[&Value::Undefined] == Value::Null)
}
fn mark_processed(&mut self, path: &[&str]) -> Result<()> {
let obj = self.processed_paths.make_or_get_value_mut(path)?;
if obj == &Value::Undefined {
*obj = Value::new_object();
}
obj.as_object_mut()?.insert(Value::Undefined, Value::Null);
Ok(())
}
fn lookup_var(&mut self, span: &Span, fields: &[&str], no_error: bool) -> Result<Value> {
let name = span.source_str();
// Return local variable/argument.
if let Some(v) = self.lookup_local_var(&name) {
return Ok(Self::get_value_chained(v, fields));
}
// Handle input.
if name.text() == "input" {
return Ok(Self::get_value_chained(self.input.clone(), fields));
}
// TODO: should we return before checking for input?
if self.no_rules_lookup {
if no_error {
return Ok(Value::Undefined);
}
return Err(span.error("undefined var"));
}
// Ensure that rules are evaluated
if name.text() == "data" {
if self.is_processed(fields)? {
return Ok(Self::get_value_chained(self.data.clone(), fields));
}
// If "data" is used in a query, without any fields, then evaluate all the modules.
if fields.is_empty() && self.active_rules.is_empty() {
for module in self.compiled_policy.modules.clone().iter() {
for rule in &module.policy {
self.eval_rule(module, rule)?;
}
}
}
// With modifiers may be used to specify part of a module that that not yet been
// evaluated. Therefore ensure that module is evaluated first.
let requested_path = format!("data.{}", fields.join("."));
self.ensure_module_evaluated(requested_path.clone())?;
for i in (1..=fields.len()).rev() {
let prefix = fields.iter().take(i).copied().collect::<Vec<_>>();
let prefix_path = format!("data.{}", prefix.join("."));
if self.compiled_policy.rules.contains_key(&prefix_path)
|| self
.compiled_policy
.default_rules
.contains_key(&prefix_path)
{
self.ensure_rule_evaluated(prefix_path)?;
break;
}
}
Ok(Self::get_value_chained(self.data.clone(), fields))
} else if !self.compiled_policy.modules.is_empty() {
let module = self.current_module()?;
let parsed_path = Parser::get_path_ref_components(&module.package.refr)?;
let mut module_var_path: Vec<&str> = parsed_path.iter().map(|s| s.text()).collect();
module_var_path.push(name.text());
if self.is_processed(&module_var_path)? {
let value = Self::get_value_chained(self.data.clone(), &module_var_path);
return Ok(Self::get_value_chained(value, fields));
}
// Ensure that all the rules having common prefix (name) are evaluated.
let rule_path = format!("data.{}", module_var_path.join("."));
if !no_error
&& !self.compiled_policy.rules.contains_key(&rule_path)
&& !self.compiled_policy.default_rules.contains_key(&rule_path)
&& !self.compiled_policy.imports.contains_key(&rule_path)
{
bail!(span.error(&format!(
"var {} is unsafe (path {:?}, scopes {:?})",
name.text(),
module_var_path,
self.scopes
)));
}
// Find the rule to which the var being looked up corresponds to. This is the prefix for
// which rules exist.
let mut found = false;
for i in (0..=fields.len()).rev() {
let comps = fields.iter().take(i).copied().collect::<Vec<_>>();
let path = if comps.is_empty() {
rule_path.clone()
} else {
format!("{}.{}", rule_path, comps.join("."))
};
if self.compiled_policy.rules.contains_key(&path)
|| self.compiled_policy.default_rules.contains_key(&path)
{
self.ensure_rule_evaluated(path)?;
found = true;
break;
}
}
if !found {
if let Some(imported_var) = self.compiled_policy.imports.get(&rule_path).cloned() {
return Ok(Self::get_value_chained(
self.eval_expr(&imported_var)?,
fields,
));
}
}
let value = Self::get_value_chained(self.data.clone(), &module_var_path[..]);
Ok(Self::get_value_chained(value, fields))
} else {
Ok(Value::Undefined)
}
}
fn eval_expr(&mut self, expr: &ExprRef) -> Result<Value> {
self.check_execution_time()?;
#[cfg(feature = "coverage")]
if self.enable_coverage {
let span = expr.span();
let source = &span.source;
let line = usize::try_from(span.line).unwrap_or(usize::MAX);
if line > 0 {
// Check if coverage table already exists for source.
match self.coverage.get_mut(source) {
Some(c) => {
// Ensure that current line is valid.
let needed = line.saturating_add(1);
if c.len() < needed {
c.resize(needed, false);
}
if let Some(slot) = c.get_mut(line) {
*slot = true;
}
}
_ => {
// Create new table.
let size = line.saturating_add(1);
let mut c = vec![false; size];
if let Some(slot) = c.get_mut(line) {
*slot = true;
}
self.coverage.insert(source.clone(), c);
}
}
}
}
match expr.as_ref() {
Expr::Null { value: v, .. }
| Expr::Bool { value: v, .. }
| Expr::Number { value: v, .. } => Ok(v.clone()),
// TODO: Handle string vs rawstring
Expr::String { value: v, .. } => Ok(v.clone()),
Expr::RawString { value: v, .. } => Ok(v.clone()),
// TODO: Handle undefined variables
Expr::Var { .. } => self.eval_chained_ref_dot_or_brack(expr),
Expr::RefDot { .. } => self.eval_chained_ref_dot_or_brack(expr),
Expr::RefBrack { .. } => self.eval_chained_ref_dot_or_brack(expr),
// Expressions with operators
Expr::ArithExpr { op, lhs, rhs, .. } => self.eval_arith_expr(expr.span(), op, lhs, rhs),
Expr::AssignExpr { .. } => {
let module_idx = self.current_module_index;
let expr_idx = expr.as_ref().eidx();
let expr_text = expr.span().text().to_string();
let binding_plan = self
.compiled_policy
.loop_hoisting_table
.get_expr_binding_plan(module_idx, expr_idx)
.map_err(|err| anyhow!("loop hoisting table out of bounds: {err}"))?
.cloned()
.ok_or_else(|| {
expr.span().error(
format!(
"binding plan missing for assignment expression (module_idx={module_idx}, expr_idx={expr_idx}, expr='{expr_text}')"
)
.as_str(),
)
})?;
let BindingPlan::Assignment { plan } = binding_plan else {
bail!(expr.span().error("internal error: not an assignment plan"));
};
self.execute_assignment_plan(&plan)
}
Expr::BinExpr { op, lhs, rhs, .. } => self.eval_bin_expr(op, lhs, rhs),
Expr::BoolExpr { op, lhs, rhs, .. } => self.eval_bool_expr(op, lhs, rhs),
Expr::Membership {
key,
value,
collection,
..
} => self.eval_membership(key, value, collection),
#[cfg(feature = "rego-extensions")]
Expr::OrExpr { lhs, rhs, .. } => {
let lhs = self.eval_expr(lhs)?;
match lhs {
Value::Bool(false) | Value::Null | Value::Undefined => self.eval_expr(rhs),
_ => Ok(lhs),
}
}
// Creation expression
Expr::Array { items, .. } => self.eval_array(items),
Expr::Object { fields, .. } => self.eval_object(fields),
Expr::Set { items, .. } => self.eval_set(items),
// Comprehensions
Expr::ArrayCompr { term, query, .. } => self.eval_array_compr(term, query),
Expr::ObjectCompr {
key, value, query, ..
} => self.eval_object_compr(key, value, query),
Expr::SetCompr { term, query, .. } => self.eval_set_compr(term, query),
Expr::UnaryExpr {
span, expr: uexpr, ..
} => match uexpr.as_ref() {
Expr::Number { .. } if !uexpr.span().text().starts_with('-') => {
builtins::numbers::arithmetic_operation(
span,
&ArithOp::Sub,
expr,
uexpr,
Value::from(0),
self.eval_expr(uexpr)?,
self.compiled_policy.strict_builtin_errors,
)
}
_ => bail!(expr
.span()
.error("unary - can only be used with numeric literals")),
},
Expr::Call {
span, fcn, params, ..
} => self.eval_call(span, expr, fcn, params, None, false),
}
}
fn make_rule_context(&self, head: &RuleHead) -> Result<(Context, Vec<Span>)> {
let module = self.current_module()?;
let mut path = Parser::get_path_ref_components(&module.package.refr)?;
match head {
RuleHead::Compr { refr, assign, .. } => {
let output_expr = assign.as_ref().map(|assign| assign.value.clone());
let (refr, key_expr, value) = match refr.as_ref() {
Expr::RefBrack { refr, index, .. } => {
(refr, Some(index.clone()), Value::new_object())
}
_ => (refr, None, Value::new_array()),
};
Parser::get_path_ref_components_into(refr, &mut path)?;
Ok((
Context {
key_expr,
output_expr,
value,
rule_ref: Some(refr.clone()),
..Context::default()
},
path,
))
}
RuleHead::Set { refr, key, .. } => {
Parser::get_path_ref_components_into(refr, &mut path)?;
let is_old_style_set = key.is_none();
Ok((
Context {
output_expr: key.clone(),
value: Value::new_set(),
rule_ref: Some(refr.clone()),
is_set: true,
is_old_style_set,
..Context::default()
},
path,
))
}
other => bail!("internal error: unhandled rule ref type: {other:?}"),
}
}
fn get_rule_module(&self, rule: &Ref<Rule>) -> Result<Ref<Module>> {
for m in self.compiled_policy.modules.iter() {
if m.policy.iter().any(|r| r == rule) {
return Ok(m.clone());
}
}
bail!("internal error: could not find module for rule");
}
fn eval_rule_bodies(
&mut self,
ctx: Context,
span: &Span,
bodies: &[RuleBody],
) -> Result<Value> {
self.check_execution_time()?;
let n_scopes = self.scopes.len();
let result = if bodies.is_empty() {
self.contexts.push(ctx.clone());
self.eval_output_expr()
} else {
let mut result = Ok(true);
for (idx, body) in bodies.iter().enumerate() {
if idx == 0 {
self.contexts.push(ctx.clone());
} else {
self.contexts.pop();
let output_expr = body.assign.as_ref().map(|e| e.value.clone());
self.contexts.push(Context {
output_expr,
// value: Value::new_array(),
// ..Context::default()
..ctx.clone()
});
}
result = self.eval_query(&body.query);
if matches!(&result, Ok(true) | Err(_)) {
break;
}
}
result
};
let popped_ctx = match self.contexts.pop() {
Some(current_ctx) => current_ctx,
_ => bail!("internal error: rule's context already popped"),
};
let result = result?;
if self.scopes.len() != n_scopes {
return Err(anyhow!("internal error: scope leak after eval_rule_bodies"));
}
if popped_ctx.rule_ref.is_some() {
if result {
return Ok(popped_ctx.rule_value);
} else {
return Ok(Value::Undefined);
}
}
Ok(match result {
true => match &popped_ctx.value {
Value::Object(_) => popped_ctx.value,
Value::Array(a) if a.len() == 1 => a
.first()
.cloned()
.ok_or_else(|| anyhow!("internal error: expected array element"))?,
Value::Array(a) if a.is_empty() => Value::Bool(true),
Value::Array(_) => {
return Err(span.source.error(
span.line,
span.col,
"complete rules should not produce multiple outputs",
))
}
Value::Set(_) => popped_ctx.value,
_ => {
return Err(anyhow!(
"internal error: unexpected ctx.value for rule evaluation: {:?}",
popped_ctx.value
));
}
},
false => Value::Undefined,
})
}
fn get_value_chained(mut obj: Value, path: &[&str]) -> Value {
for p in path {
obj = obj[&Value::String(p.to_string().into())].clone();
}
obj
}
#[inline]
pub fn make_or_get_value_mut<'a>(obj: &'a mut Value, paths: &[&str]) -> Result<&'a mut Value> {
let (first, tail) = match paths.split_first() {
Some(v) => v,
None => return Ok(obj),
};
let key = Value::String((*first).into());
if obj == &Value::Undefined {
*obj = Value::new_object();
}
if let Value::Object(map) = obj {
if map.get(&key).is_none() {
Rc::make_mut(map).insert(key.clone(), Value::Undefined);
}
}
match obj {
Value::Object(map) => match Rc::make_mut(map).get_mut(&key) {
Some(v) if tail.is_empty() => Ok(v),
Some(v) => Self::make_or_get_value_mut(v, tail),
_ => bail!("internal error: unexpected"),
},
Value::Undefined if !tail.is_empty() => {
*obj = Value::new_object();
Self::make_or_get_value_mut(obj, paths)
}
Value::Undefined => Ok(obj),
_ => bail!("internal error: make: not an object {obj:?}"),
}
}
pub fn merge_rule_value(span: &Span, value: &mut Value, new: Value) -> Result<()> {
match value.merge(new) {
Ok(()) => Ok(()),
Err(_) => Err(span.error("rules should not produce multiple outputs.")),
}
}
pub fn get_path_string(refr: &Expr, document: Option<&str>) -> Result<String> {
let mut comps = vec![];
let mut expr_opt = Some(refr);
while let Some(expr) = expr_opt {
match expr {
Expr::RefDot {
refr: nested_refr,
field,
..
} => {
comps.push(field.0.text());
expr_opt = Some(nested_refr);
}
Expr::RefBrack {
refr: nested_refr,
index,
..
} if matches!(index.as_ref(), Expr::String { .. }) => {
if let Expr::String { span: s, .. } = index.as_ref() {
comps.push(s.text());
expr_opt = Some(nested_refr);
}
}
Expr::Var { span: v, .. } => {
comps.push(v.text());
expr_opt = None;
}
_ => bail!(expr.span().error("invalid ref expression")),
}
}
if let Some(doc_component) = document {
comps.push(doc_component);
};
comps.reverse();
Ok(comps.join("."))
}
pub fn set_current_module(
&mut self,
module: Option<Ref<Module>>,
) -> Result<Option<Ref<Module>>> {
let previous_module = self.module.clone();
if let Some(new_module) = &module {
self.current_module_path =
Self::get_path_string(&new_module.package.refr, Some("data"))?;
self.current_module_index = self.find_module_index(new_module);
}
self.module = module;
Ok(previous_module)
}
fn find_module_index(&self, module: &Ref<Module>) -> u32 {
self.compiled_policy
.modules
.iter()
.position(|m| core::ptr::eq(m.as_ref(), module.as_ref()))
.map_or_else(
|| {
self.query_module.as_ref().map_or(0, |query_module| {
if core::ptr::eq(query_module.as_ref(), module.as_ref()) {
u32::try_from(self.compiled_policy.modules.len()).unwrap_or(u32::MAX)
} else {
0
}
})
},
|idx| u32::try_from(idx).unwrap_or(u32::MAX),
)
}
const fn get_rule_refr(rule: &Rule) -> &ExprRef {
match rule {
Rule::Spec { head, .. } => match &head {
RuleHead::Compr { refr, .. }
| RuleHead::Set { refr, .. }
| RuleHead::Func { refr, .. } => refr,
},
Rule::Default { refr, .. } => refr,
}
}
fn check_default_value(expr: &ExprRef) -> Result<()> {
use Expr::*;
let (kind, span) = match expr.as_ref() {
// Scalars are supported
String { .. } | RawString { .. } | Number { .. } | Bool { .. } | Null { .. } => {
return Ok(())
}
// Uminus of number is treated as a single expression,
UnaryExpr { expr, .. } if matches!(expr.as_ref(), Number { .. }) => return Ok(()),
// Comprehensions are supported since they won't evaluate to undefined.
ArrayCompr { .. } | SetCompr { .. } | ObjectCompr { .. } => return Ok(()),
// Check each item in array/set.
Array { items, .. } | Set { items, .. } => {
for item in items {
Self::check_default_value(item)?;
}
return Ok(());
}
// Check each field in object
Object { fields, .. } => {
for (_, key, value) in fields {
Self::check_default_value(key)?;
Self::check_default_value(value)?;
}
return Ok(());
}
// The following may evaluate to undefined.
Var { span, .. } => ("var", span),
Call { span, .. } => ("call", span),
UnaryExpr { span, .. } => ("unaryexpr", span),
RefDot { span, .. } => ("ref", span),
RefBrack { span, .. } => ("ref", span),
BinExpr { span, .. } => ("binexpr", span),
BoolExpr { span, .. } => ("boolexpr", span),
ArithExpr { span, .. } => ("arithexpr", span),
AssignExpr { span, .. } => ("assignexpr", span),
Membership { span, .. } => ("membership", span),
#[cfg(feature = "rego-extensions")]
OrExpr { span, .. } => ("orexpr", span),
};
Err(span.error(format!("invalid `{kind}` in default value").as_str()))
}
pub fn check_default_rules(&self) -> Result<()> {
for module in self.compiled_policy.modules.iter() {
for rule in &module.policy {
if let Rule::Default { value, .. } = rule.as_ref() {
Self::check_default_value(value)?;
}
}
}
Ok(())
}
pub fn eval_default_rule(&mut self, rule: &Ref<Rule>) -> Result<()> {
self.check_execution_time()?;
// Skip reprocessing rule.
if self.processed.contains(rule) {
return Ok(());
}
if let Rule::Default {
span,
refr,
value,
args,
..
} = rule.as_ref()
{
if !args.is_empty() {
// Non-zero function defaults are evaluated differently.
return Ok(());
}
let scopes = core::mem::take(&mut self.scopes);
let module = self.current_module()?;
let mut path = Parser::get_path_ref_components(&module.package.refr)?;
let (refr, index) = match refr.as_ref() {
Expr::RefBrack { refr, index, .. } => (refr, Some(index.clone())),
Expr::RefDot { .. } => (refr, None),
Expr::Var { .. } => (refr, None),
_ => bail!(refr
.span()
.error(&format!("invalid token {refr:?} with the default keyword"))),
};
Parser::get_path_ref_components_into(refr, &mut path)?;
let paths: Vec<&str> = path.iter().map(|s| s.text()).collect();
Self::check_default_value(value)?;
let value = self.eval_expr(value)?;
// Assume at this point that all the non-default rules have been evaluated.
// Merge the default value only if
// 1. The corresponding variable does not have value yet
// 2. The corresponding index in the object does not have value yet
if let Some(index) = index {
let index = self.eval_expr(&index)?;
let mut object = Value::new_object();
object.as_object_mut()?.insert(index.clone(), value);
let vref = Self::make_or_get_value_mut(&mut self.data, &paths)?;
if let Value::Object(btree) = &vref {
if !btree.contains_key(&index) {
Self::merge_rule_value(span, vref, object)?;
}
} else if let Value::Undefined = vref {
Self::merge_rule_value(span, vref, object)?;
}
} else {
let vref = Self::make_or_get_value_mut(&mut self.data, &paths)?;
if let Value::Undefined = &vref {
Self::merge_rule_value(span, vref, value)?;
}
};
self.scopes = scopes;
self.processed.insert(rule.clone());
}
Ok(())
}
/// Evaluate a default rule and return the resulting value for compiler consumers.
#[cfg(feature = "rvm")]
pub fn eval_default_rule_for_compiler(&mut self, rule_path: &str) -> Result<Value> {
self.check_execution_time()?;
self.input = Value::Undefined;
self.data = Value::Undefined;
self.ensure_loop_var_values_capacity();
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,
_refr: &Expr,
path: &[&str],
value: Value,
) -> Result<()> {
if value == Value::Undefined {
return Ok(());
}
// Ensure that path is created.
let vref = Self::make_or_get_value_mut(&mut self.data, path)?;
if Self::get_value_chained(self.init_data.clone(), path) == Value::Undefined {
Self::merge_rule_value(span, vref, value)
} else {
// Retain specified value.
Ok(())
}
}
fn check_rule_path(
&mut self,
refr: &ExprRef,
path: &[Value],
value: &Value,
is_set: bool,
) -> Result<()> {
// TODO: can copying of path be avoided below?
let range = self.rule_values.range((Unbounded, Included(path.to_vec())));
// Check whether any rules evaluated so far is a parent of given rule.
let mut conflict = None;
for (k, (v, r)) in range.rev() {
if path.starts_with(k) {
if k == path && (is_set || v == value) {
// rule evaluated to same value again.
} else {
conflict = Some((v, r));
}
} else {
break;
}
}
if let Some((_, r)) = conflict {
bail!(refr.span().error(&format!(
"rule conflicts with the following rule:\n{}",
r.span().message("", "defined here")
)));
}
self.rule_values
.insert(path.to_vec(), (value.clone(), refr.clone()));
Ok(())
}
fn eval_rule_impl(&mut self, module: &Ref<Module>, rule: &Ref<Rule>) -> Result<()> {
self.check_execution_time()?;
match rule.as_ref() {
Rule::Spec {
span,
head: rule_head,
bodies: rule_body,
} => {
match rule_head {
RuleHead::Compr { refr, .. } | RuleHead::Set { refr, .. } => {
let (ctx, _) = self.make_rule_context(rule_head)?;
let is_set = ctx.is_set;
let is_object = ctx.key_expr.is_some() && !is_set;
let value = self.eval_rule_bodies(ctx, span, rule_body)?;
let package_components = self.eval_rule_ref(&module.package.refr)?;
if value != Value::Undefined {
for (path, value_in_map) in value.as_object()? {
let mut full_path = package_components.clone();
full_path.append(&mut path.as_array()?.clone());
self.check_rule_path(refr, &full_path, value_in_map, is_set)?;
self.update_rule_value(
span,
full_path,
value_in_map.clone(),
is_set,
)?;
}
} else if is_set {
if let Ok(mut comps) = self.eval_rule_ref(refr) {
let mut full_path = package_components;
full_path.append(&mut comps);
self.update_rule_value(span, full_path, Value::new_set(), true)?;
}
} else if is_object {
// Fetch the rule, ignoring the key.
if let Expr::RefBrack { refr, .. } = refr.as_ref() {
if let Ok(mut comps) = self.eval_rule_ref(refr) {
let mut full_path = package_components;
full_path.append(&mut comps);
self.update_rule_value(
span,
full_path,
Value::Undefined,
false,
)?;
}
}
}
self.processed.insert(rule.clone());
}
RuleHead::Func {
refr, args, assign, ..
} => {
let mut path =
Parser::get_path_ref_components(&self.current_module()?.package.refr)?;
Parser::get_path_ref_components_into(refr, &mut path)?;
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
// Ensure that for functions with a nesting level (e.g: a.foo),
// `a` is created as an empty object.
if let Some((_, prefix)) = path.split_last() {
if !prefix.is_empty() {
self.update_data(span, refr, prefix, Value::new_object())?;
}
}
if args.is_empty() {
let ctx = Context {
output_expr: assign.as_ref().map(|a| a.value.clone()),
value: Value::new_array(),
..Context::default()
};
let value = self.eval_rule_bodies(ctx, span, rule_body)?;
self.update_data(refr.span(), refr, &path[..], value)?;
}
}
}
}
_ => bail!("internal error: unexpected"),
}
Ok(())
}
pub fn eval_rule(&mut self, module: &Ref<Module>, rule: &Ref<Rule>) -> Result<()> {
self.check_execution_time()?;
// Set current module index
self.current_module_index = self.find_module_index(module);
// Skip reprocessing rule
if self.processed.contains(rule) {
return Ok(());
}
// Skip default rules
if let Rule::Default { .. } = rule.as_ref() {
return Ok(());
}
self.active_rules.push(rule.clone());
if self.active_rules.iter().filter(|&r| r == rule).count() == 2 {
let mut msg = String::default();
for r in &self.active_rules {
let refr = Self::get_rule_refr(r);
let span = refr.span();
msg.push_str(
span.source
.message(span.line, span.col, "depends on", "")
.as_str(),
);
}
msg.push_str("cyclic evaluation");
self.active_rules.pop();
let refr = Self::get_rule_refr(rule);
let span = refr.span();
return Err(span.source.error(
span.line,
span.col,
format!("recursion detected when evaluating rule:{msg}").as_str(),
));
}
// Back up local variables of current function and empty
// the local variables of callee function.
let scopes = core::mem::take(&mut self.scopes);
let prev_module = self.set_current_module(Some(module.clone()))?;
let res = self.eval_rule_impl(module, rule);
self.set_current_module(prev_module)?;
self.scopes = scopes;
match self.active_rules.pop() {
Some(ref r) if r == rule => res,
_ => bail!("internal error: current rule not active"),
}
}
pub fn eval_user_query(
&mut self,
module: &Ref<Module>,
query: &Ref<Query>,
query_schedule: Schedule,
enable_tracing: bool,
) -> Result<QueryResults> {
self.check_execution_time()?;
self.traces = match enable_tracing {
true => Some(vec![]),
false => None,
};
self.memory_check()?;
// Store the query schedule for lookup during evaluation
self.query_schedule = Some(query_schedule);
// Push new context.
self.contexts.push(Context {
value: Value::new_set(),
// Request that results be gathered.
result: Some(QueryResult::default()),
..Context::default()
});
self.query_module = Some(module.clone());
let prev_module = self.set_current_module(Some(module.clone()))?;
// For user queries, set the module index to match the schedule
// Query snippets are scheduled as if they're in a module at the end
let prev_module_index = self.current_module_index;
let compiled_modules_len =
u32::try_from(self.compiled_policy.modules.len()).unwrap_or(u32::MAX);
self.current_module_index = compiled_modules_len;
self.ensure_loop_var_values_capacity();
// Eval the query.
let query_r = self.eval_query(query);
self.query_module = None;
let mut results = match self.contexts.pop() {
Some(ctx) => ctx.results,
_ => bail!("internal error: no context"),
};
// Apply expression ordering from the schedule when it is safe to do so.
// If the schedule references statements that did not produce expressions, the lengths
// will differ; in that case we keep the collected order to avoid spurious errors.
// Example: a schedule for `1 == 2; 1 == 1` may list both statements. The first produces
// an expression (false), but evaluation stops and the second never runs, so only one
// expression is collected. In that case `order.len()` can be 2 while one expression is
// available. In these cases, we avoid reordering - doing so requires maintaining additional
// data during evaluation and is not worth the complexity for now.
let current_module_idx = compiled_modules_len;
let current_query_idx = query.qidx;
if let Some(ref self_schedule) = &self.query_schedule {
if let Some(active_schedule) = self_schedule
.queries
.get_checked(current_module_idx, current_query_idx)
.map_err(|err| anyhow!("schedule out of bounds: {err}"))?
{
for result in results.result.iter_mut() {
let exprs_len = result.expressions.len();
// Skip reordering when the schedule length does not match produced expressions.
if active_schedule.order.len() != exprs_len {
continue;
}
let placeholder = Expression {
value: Value::Undefined,
text: "".into(),
location: Location { row: 0, col: 0 },
};
let mut ordered_expressions = vec![placeholder; exprs_len];
let mut invalid = false;
for (expr_idx, value) in result.expressions.iter().enumerate() {
let Some(&order_idx) = active_schedule.order.get(expr_idx) else {
invalid = true;
break;
};
let orig_idx = usize::from(order_idx);
if let Some(slot) = ordered_expressions.get_mut(orig_idx) {
*slot = value.clone();
} else {
invalid = true;
break;
}
}
if !invalid
&& !ordered_expressions
.iter()
.any(|v| v.value == Value::Undefined)
{
result.expressions = ordered_expressions;
}
}
}
}
// Clear the query schedule
self.query_schedule = None;
self.set_current_module(prev_module)?;
self.current_module_index = prev_module_index;
if let Some(r) = results.result.last() {
if matches!(&r.bindings, Value::Object(obj) if obj.is_empty())
&& (r.expressions.len() > 1
&& r.expressions.iter().any(|e| e.value == Value::Bool(false)))
{
results = QueryResults::default();
}
}
match query_r {
Ok(_) => Ok(results),
Err(e) => Err(e),
}
}
fn get_rule_path_components(mut refr: &Ref<Expr>) -> Result<Vec<Rc<str>>> {
let mut components: Vec<Rc<str>> = vec![];
loop {
refr = match refr.as_ref() {
Expr::Var { span: v, .. } => {
components.push(v.text().into());
break;
}
Expr::RefBrack { refr, index, .. } => {
if let Expr::String { span: s, .. } = index.as_ref() {
components.push(s.text().into());
} else {
components.clear();
}
refr
}
Expr::RefDot { refr, field, .. } => {
components.push(field.0.text().into());
refr
}
_ => break,
}
}
components.reverse();
Ok(components)
}
pub fn create_rule_prefixes(&mut self) -> Result<()> {
for module in self.compiled_policy.modules.clone().iter() {
let module_path = Self::get_rule_path_components(&module.package.refr)?;
for rule in &module.policy {
let rule_refr = Self::get_rule_refr(rule);
let mut prefix_path = module_path.clone();
let mut components = Self::get_rule_path_components(rule_refr)?;
let is_old_set = matches!(
rule.as_ref(),
Rule::Spec {
head: RuleHead::Set { key: None, .. },
..
}
);
if components.len() >= 2 && is_old_set {
components.pop();
}
if components.len() <= 1 {
continue;
}
components.pop();
prefix_path.append(&mut components);
let prefix_path: Vec<&str> = prefix_path.iter().map(|s| s.as_ref()).collect();
if Self::get_value_chained(self.data.clone(), &prefix_path) == Value::Undefined {
self.update_data(
rule_refr.span(),
rule_refr,
&prefix_path,
Value::new_object(),
)?;
}
}
}
Ok(())
}
fn record_rule(&mut self, refr: &Ref<Expr>, rule: Ref<Rule>) -> Result<()> {
let comps = Parser::get_path_ref_components(refr)?;
let comps: Vec<&str> = comps.iter().map(|s| s.text()).collect();
for (c, _) in comps.iter().enumerate() {
let path = format!(
"{}.{}",
self.current_module_path,
comps
.iter()
.take(c.saturating_add(1))
.copied()
.collect::<Vec<_>>()
.join(".")
);
if c == comps.len().saturating_sub(1) {
self.compiled_policy_mut().rule_paths.insert(path.clone());
}
match self.compiled_policy_mut().rules.entry(path) {
MapEntry::Occupied(o) => {
o.into_mut().push(rule.clone());
}
MapEntry::Vacant(v) => {
v.insert(vec![rule.clone()]);
}
}
}
Ok(())
}
fn record_default_rule(
&mut self,
refr: &Ref<Expr>,
rule: &Ref<Rule>,
index: Option<String>,
) -> Result<()> {
let comps = Parser::get_path_ref_components(refr)?;
let comps: Vec<&str> = comps.iter().map(|s| s.text()).collect();
for (idx, comp_idx) in (0..comps.len()).enumerate() {
let path = format!(
"{}.{}",
self.current_module_path,
comps
.iter()
.take(comp_idx.saturating_add(1))
.copied()
.collect::<Vec<_>>()
.join(".")
);
if comp_idx == comps.len().saturating_sub(1) {
self.compiled_policy_mut().rule_paths.insert(path.clone());
}
match self.compiled_policy_mut().default_rules.entry(path) {
MapEntry::Occupied(o) => {
if idx == comps.len().saturating_sub(1) {
for (_, i) in o.get() {
if let (Some(old), Some(new)) = (i, &index) {
if old == new {
bail!(refr.span().error("multiple default rules for the variable with the same index"));
}
} else if index.is_some() || i.is_some() {
bail!(refr.span().error("conflict type with the default rules"));
}
}
}
o.into_mut().push((rule.clone(), index.clone()));
}
MapEntry::Vacant(v) => {
v.insert(vec![(rule.clone(), index.clone())]);
}
}
}
Ok(())
}
pub fn process_imports(&mut self) -> Result<()> {
for module in self.compiled_policy.modules.clone().iter() {
let module_path = get_path_string(&module.package.refr, Some("data"))?;
for import in &module.imports {
let target = match &import.r#as {
Some(s) => s.text(),
_ => match import.refr.as_ref() {
Expr::RefDot { field, .. } => field.0.text(),
Expr::RefBrack { index, .. } => match index.as_ref() {
Expr::String { span: s, .. } => s.text(),
_ => "",
},
Expr::Var { span: v, .. } if v.text() == "input" => {
// Warn redundant import of input. Ignore it.
#[cfg(feature = "std")]
std::eprintln!(
"{}",
import
.refr
.span()
.message("warning", "redundant import of `input`")
);
continue;
}
_ => "",
},
};
if target.is_empty() {
bail!(import
.refr
.span()
.message("warning", "invalid ref in import"));
}
self.compiled_policy_mut()
.imports
.insert(format!("{}.{}", module_path, target), import.refr.clone());
}
}
Ok(())
}
pub fn gather_rules(&mut self) -> Result<()> {
for module in self.compiled_policy.modules.clone().iter() {
let prev_module = self.set_current_module(Some(module.clone()))?;
for rule in &module.policy {
let refr = Self::get_rule_refr(rule);
if let Rule::Spec { .. } = rule.as_ref() {
// Adjust refr to ensure simple ref.
// TODO: refactor.
let refr = match refr.as_ref() {
Expr::RefBrack { index, .. }
if matches!(index.as_ref(), Expr::String { .. }) =>
{
refr
}
Expr::RefBrack { refr, .. } => refr,
_ => refr,
};
self.record_rule(refr, rule.clone())?;
} else if let Rule::Default { .. } = rule.as_ref() {
let (refr, index) = match refr.as_ref() {
// TODO: Validate the index
Expr::RefBrack { refr, index, .. } => {
if !matches!(
index.as_ref(),
Expr::Bool { .. } | Expr::Number { .. } | Expr::String { .. }
) {
// OPA's behavior is ignoring the non-scalar index
bail!(index.span().error("index is not a scalar value"));
}
let index = self.eval_expr(index)?;
(refr, Some(index.to_string()))
}
_ => (refr, None),
};
self.record_default_rule(refr, rule, index)?;
}
}
self.set_current_module(prev_module)?;
}
Ok(())
}
pub fn add_extension(
&mut self,
path: String,
nargs: u8,
extension: Box<dyn Extension>,
) -> Result<()> {
if let MapEntry::Vacant(v) = self.extensions.entry(path) {
v.insert((nargs, Rc::new(extension)));
Ok(())
} else {
bail!("extension already added");
}
}
#[cfg(feature = "coverage")]
fn gather_coverage_in_query(
&self,
query: &Ref<Query>,
covered: &Vec<bool>,
file: &mut crate::coverage::File,
) -> Result<()> {
for stmt in &query.stmts {
// TODO: with mods
match &stmt.literal {
Literal::SomeVars { .. } => (),
Literal::SomeIn {
value, collection, ..
} => {
self.gather_coverage_in_expr(value, covered, file)?;
self.gather_coverage_in_expr(collection, covered, file)?;
}
Literal::Expr { expr, .. } | Literal::NotExpr { expr, .. } => {
self.gather_coverage_in_expr(expr, covered, file)?;
}
Literal::Every {
domain,
query: nested_query,
..
} => {
self.gather_coverage_in_expr(domain, covered, file)?;
self.gather_coverage_in_query(nested_query, covered, file)?;
}
}
}
Ok(())
}
#[cfg(feature = "coverage")]
fn gather_coverage_in_expr(
&self,
expr: &Ref<Expr>,
covered: &Vec<bool>,
file: &mut crate::coverage::File,
) -> Result<()> {
use Expr::*;
traverse(expr, &mut |e| {
Ok(match e.as_ref() {
ArrayCompr { query, .. } | SetCompr { query, .. } | ObjectCompr { query, .. } => {
self.gather_coverage_in_query(query, covered, file)?;
false
}
_ => {
let line_usize = usize::try_from(e.span().line).unwrap_or(usize::MAX);
match covered.get(line_usize) {
Some(true) => {
let line_u32 = u32::try_from(line_usize).unwrap_or(u32::MAX);
file.covered.insert(line_u32);
}
Some(false) | None => {
let line_u32 = u32::try_from(line_usize).unwrap_or(u32::MAX);
file.not_covered.insert(line_u32);
}
}
true
}
})
})?;
Ok(())
}
#[cfg(feature = "coverage")]
pub fn get_coverage_report(&self) -> Result<crate::coverage::Report> {
let mut report = crate::coverage::Report::default();
for module in self.compiled_policy.modules.iter() {
let span = module.package.refr.span();
// Get coverage information for the module.
let Some(covered) = self.coverage.get(&span.source) else {
continue;
};
let mut file = crate::coverage::File {
path: span.source.file().clone(),
code: span.source.contents().clone(),
covered: BTreeSet::new(),
not_covered: BTreeSet::new(),
};
// Loop through each rule and figure out the lines that were not coverd.
for rule in &module.policy {
match rule.as_ref() {
Rule::Spec { head, bodies, .. } => {
match head {
RuleHead::Compr { assign, .. } | RuleHead::Func { assign, .. } => {
if let Some(a) = assign {
self.gather_coverage_in_expr(&a.value, covered, &mut file)?;
}
}
RuleHead::Set { key, .. } => {
if let Some(k) = key {
self.gather_coverage_in_expr(k, covered, &mut file)?;
}
}
}
for b in bodies {
self.gather_coverage_in_query(&b.query, covered, &mut file)?;
}
}
Rule::Default { value, .. } => {
self.gather_coverage_in_expr(value, covered, &mut file)?;
}
}
}
report.files.push(file);
}
Ok(report)
}
#[cfg(feature = "coverage")]
pub fn set_enable_coverage(&mut self, enable: bool) {
if self.enable_coverage != enable {
self.enable_coverage = enable;
self.clear_coverage_data();
}
}
#[cfg(feature = "coverage")]
pub fn clear_coverage_data(&mut self) {
self.coverage = Map::new();
}
pub fn set_gather_prints(&mut self, b: bool) {
if b != self.gather_prints {
// Clear existing prints.
core::mem::take(&mut self.prints);
}
self.gather_prints = b;
}
pub fn take_prints(&mut self) -> Result<Vec<String>> {
Ok(core::mem::take(&mut self.prints))
}
pub fn eval_rule_in_path(&mut self, path: String) -> Result<Value> {
self.check_execution_time()?;
if !self.compiled_policy.rule_paths.contains(&path) {
bail!("not a valid rule path");
}
self.ensure_rule_evaluated(path.clone())?;
let parts: Vec<&str> = path.split('.').collect();
let (_, tail) = parts
.split_first()
.ok_or_else(|| anyhow!("internal error: expected rule path"))?;
let value = Self::get_value_chained(self.data.clone(), tail);
#[cfg(feature = "azure_policy")]
{
if let Some(target_info) = &self.compiled_policy.target_info {
// Allow undefined values to pass through without schema validation
if value != Value::Undefined {
target_info.effect_schema.validate(&value)?;
}
}
}
Ok(value)
}
pub fn compile(&mut self, rule: Option<Rc<str>>) -> Result<Rc<CompiledPolicyData>> {
let data = Some(self.init_data.clone());
let extensions = self.extensions.clone();
let compiled_policy = self.compiled_policy_mut();
compiled_policy.data = data;
compiled_policy.extensions = extensions;
if let Some(rule) = rule {
if !compiled_policy.rule_paths.contains(rule.as_ref()) {
bail!("not a valid rule path");
}
compiled_policy.rule_to_evaluate = rule;
} else {
compiled_policy.rule_to_evaluate = "".into();
}
// Populate loop hoisting lookup table
use crate::compiler::hoist::LoopHoister;
// Re-run hoisting with the analyzer's schedule so statement order is preserved.
let hoister = compiled_policy
.schedule
.clone()
.map_or_else(LoopHoister::new, LoopHoister::new_with_schedule);
let loop_lookup = hoister.populate(compiled_policy.modules.as_ref())?;
compiled_policy.loop_hoisting_table = loop_lookup;
Ok(self.compiled_policy.clone())
}
}