Files
regorus/src/interpreter.rs
Mark Birger 9a486c79bf fix: Deep-merge nested data documents in Engine::add_data (#760)
* Deep-merge nested data documents in Engine::add_data

add_data previously performed a shallow merge: adding a nested object under a key that already existed either replaced the whole subtree or errored on a spurious conflict, instead of merging the trees. This makes Engine::add_data (and the shared Value::merge) recurse into nested objects so keys from both sides are preserved, matching OPA's data-document merge semantics. Nested sets are unioned as a regorus extension (OPA data is JSON and has no sets). Genuine leaf conflicts (same path, two different scalar values) still error; equal values remain a no-op, which the shared rule-evaluation path relies on. Adds tests for object deep-merge, set union, leaf/type conflicts, and interaction with the 'with data.x' modifier.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* docs(value): clarify Value::merge conflict wording

Copilot review on #760 noted the doc comment called non-mergeable variants 'non-container values', which is misleading since arrays are containers yet still conflict unless equal. Reword to describe a conflict as any differing pair that is not both objects or both sets (e.g. unequal scalars or arrays).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* perf(value): avoid deep-cloning RHS set during merge union

When unioning sets in Value::merge, the RHS set is often shared: the object arm recurses via existing.merge(v.clone()), which bumps the incoming set's Rc refcount. The old Rc::make_mut(new) then structurally deep-cloned the entire RHS BTreeSet just to drain it via append and immediately discard the copy.

Move the elements out when the RHS set is uniquely owned, and otherwise clone only the per-element Rc handles into the destination. The union result is identical (BTreeSet dedups), but no throwaway set is allocated on the nested-merge path exercised by add_data deep-merge.

Addresses a Copilot review comment on #760.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(engine): make add_data atomic on merge conflict

Now that Value::merge recurses, a conflict in a later nested key was reported only after earlier keys of the same document had already been written into the live init_data, leaving the engine partially mutated on a rejected add_data.

Add a read-only Value::check_mergeable that mirrors merge's conflict rule (objects deep-merge, sets union, equal values no-op, anything else conflicts) and run it in add_data before merging. On conflict nothing is mutated, so add_data is all-or-nothing. The check allocates nothing and never copies the data spine, preserving merge's in-place uniquely-owned fast path (no candidate copy of the data document).

Adds regression tests for a partial object-leaf conflict and a partial set-union conflict. Reported by a maintainer on #760.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test(engine): add array atomicity regression for add_data

Arrays are atomic leaves, so a differing array at a shared path is a
conflict. The new key sorts before the conflicting array key, so a naive
in-place merge would leak the new key before hitting the conflict. This
test locks in that add_data rejects the whole call and leaves data
untouched.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix: make add_data atomic under allocator memory limits

On llocator-memory-limits builds, Value::merge runs the limit check
*after* inserting each key, so an add_data whose merge trips the limit
mid-way left the data document partially mutated. check_mergeable only
models semantic conflicts, not limit failures, so the validate-then-merge
precheck couldn't cover this failure mode.

Use a build-split strategy in dd_data:
- default builds: keep the zero-copy validate-then-merge fast path
  (a conflict is the only way the merge can fail).
- allocator-memory-limits builds: merge into a candidate copy and commit
  only on success, making both conflict and limit failures transactional.
  Value is Rc/copy-on-write, so only touched subtrees are cloned.

check_mergeable is now cfg-gated to the default build to avoid dead code.

Tests (allocator-memory-limits build): add a partial-merge atomicity test
(limit trips mid-merge, data must be untouched) and a candidate-copy
conflict-atomicity test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix: separate strict rule-output merge from data-document deep-merge

#760 made Value::merge recursive so Engine::add_data deep-merges nested
data documents. But that same method also backs rule materialization,
where recursion is wrong: two rule definitions producing different
outputs for one path must conflict (OPA complete-rule semantics), not
silently combine.

Split the two behaviors:
- Value::merge is strict and shallow again (as pre-#760): a key on both
  sides must be equal or it conflicts; used for rule outputs.
- Value::deep_merge is the recursive data-document merge behind add_data;
  check_mergeable validates it up front without allocating, so the
  default build merges in place instead of cloning a candidate.

Also fix zero-arg functions (f() := ...): route their materialization
through strict equality via a new RuleValueMerge selector, so disjoint
outputs ({a:1} vs {b:2}) conflict as OPA does while prefix scaffolding
(a.foo + a.bar) still combines.

Add a 14-case interpreter conformance matrix (multiple_outputs.yaml)
covering functions, static/dynamic partial objects, and ref-heads,
matched against OPA v1.2.0.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* perf(value): make deep_merge acquire mutable access lazily

deep_merge's object arm called Rc::make_mut on the target map up front,
cloning a shared map's spine even when the merge changed nothing (a
no-op subset re-add) or conflicted before any mutation. Decide each
incoming key from a read-only probe (skip / insert / recurse / conflict)
and take Rc::make_mut only when a key actually mutates, so no-op and
conflict merges leave shared maps untouched.

Behavior is unchanged: the equality short-circuit that previously ran
inside the recursive call now runs in the probe, and conflicts bail with
the same message. Add value tests asserting Rc::ptr_eq is preserved
across no-op subset, equal-nested-object, and first-key-conflict merges.

OPA conformance unchanged (3021 pass / 651 fail, byte-identical).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* feat(value): bound deep_merge recursion depth to prevent stack-overflow DoS

deep_merge and check_mergeable recursed unbounded on object/set nesting.
A Value built without serde_json's parse-time recursion limit (the Python
and Ruby native bindings, or programmatic construction) could therefore
drive add_data into a stack overflow -- an uncatchable abort that poisons
every engine in an FFI process.

Thread a depth counter through both functions and bail past MAX_MERGE_DEPTH
(128, matching serde_json's default) so over-deep data fails with a clean
Err. In the default build check_mergeable trips first, keeping add_data
atomic; the guard in deep_merge covers the allocator-memory-limits build
and any disjoint-then-overlapping merge.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* docs(changelog): note strict zero-arg function conflict and add_data depth limit

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Mark Birger <markbirger@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-21 15:18:00 -05:00

4468 lines
164 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::{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),
}
/// How [`Interpreter::update_data`] merges a rule's value into the data document.
#[derive(Debug, Clone, Copy)]
enum RuleValueMerge {
/// Shallow-merge keeping disjoint keys, so rules sharing a path prefix scaffold into one
/// object (`a.foo` + `a.bar` → one `a`) instead of conflicting.
Combine,
/// Complete-rule semantics: existing value must be absent or exactly equal, else conflict.
/// Used for zero-arg function outputs (`f() := …`), which OPA treats like complete rules.
Strict,
}
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()?.get_or_insert_with(
Value::String(p.to_string().into()),
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")))?
.get_or_insert_with(p, 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")))?;
if value == Value::Undefined {
// TODO: clean this assumption between Undefined vs Object.
obj.get_or_insert_with(p, Value::new_object);
} else {
let existing = obj.get_or_insert_with(p, || value.clone());
if *existing != value {
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")))?
.get_or_insert_with(p, 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 {
is_const_rule = is_const_rule && Self::is_simple_literal(ke)?;
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()?
.get_or_insert_with(Value::from_array(comps), Value::new_set);
if output != Value::Undefined {
set.as_set_mut()?.insert(output);
return Ok(true);
}
return Ok(false);
}
// Non-set rule.
let key = Value::from_array(comps);
let obj_mut = ctx_mut.rule_value.as_object_mut()?;
let existing = obj_mut.get_or_insert_with(key, || output.clone());
if *existing != output {
bail!(rule_ref
.span()
.error("rules must not produce multiple outputs"));
}
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,
// Resource-limit errors must always propagate, even in non-strict
// mode, to prevent `not builtin(...)` from silently flipping to true.
Err(e) if !self.compiled_policy.strict_builtin_errors => {
if e.downcast_ref::<crate::LimitError>().is_some() {
return Err(e);
}
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_sorted().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.")),
}
}
/// Materialize a complete-rule value: the existing value must be absent or *exactly equal*
/// to `new`, else it is a conflict.
///
/// Unlike the shallow [`Self::merge_rule_value`], differing outputs conflict instead of
/// combining — `f() := {"a": 1}` and `f() := {"b": 2}` conflict — matching OPA's semantics
/// for zero-arg functions.
fn merge_rule_value_strict(span: &Span, value: &mut Value, new: Value) -> Result<()> {
if *value == Value::Undefined {
*value = new;
Ok(())
} else if *value == new {
Ok(())
} else {
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,
merge: RuleValueMerge,
) -> 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 {
match merge {
RuleValueMerge::Strict => Self::merge_rule_value_strict(span, vref, value),
RuleValueMerge::Combine => 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(),
RuleValueMerge::Combine,
)?;
}
}
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,
RuleValueMerge::Strict,
)?;
}
}
}
}
_ => 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(),
RuleValueMerge::Combine,
)?;
}
}
}
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())
}
}