Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion docs/rvm/instruction-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ comprehension contexts. Outside loops it aborts the current rule.
| Mnemonic | Operands / Params | Behaviour |
| :------------------------- | :---------------------------- | :---------------------------------------------------------- |
| `ObjectSet` | `obj, key, value` | Mutates object in `obj` with key/value from registers. |
| `ObjectSetAdd` | `obj, key, value` | Adds a defined value to the set at `obj[key]`, creating it if absent. Undefined keys and values are omitted. |
| `ObjectCreate` | `params_index=pN` | Builds object from literal template and register entries. |
| `ArrayNew` | `dest` | Creates empty array. |
| `ArrayPush` | `arr, value` | Appends to array. |
Expand All @@ -84,6 +85,48 @@ comprehension contexts. Outside loops it aborts the current rule.

Parameter structures:

- `ObjectSetAdd` uses three register operands without a parameter table. It
accumulates `rule[key] contains value` into an object of sets, preserving
existing members and omitting undefined keys and values. The target must be
an object, and an existing leaf must be a set even when the incoming value is
undefined. An undefined contribution does not create a missing leaf. Static
string prefixes remain part of the rule path; when a parent rule also has dynamic keys, its literal
final keys accumulate into the same object. Head-key loops are hoisted even
for bodyless rules and independently for each body. Undefined and scalar
collections contribute nothing. Failed keyed
rule bodies do not create empty leaf sets. The compiler
rejects conflicting `contains` and single-valued rule kinds. Multiple dynamic
key levels remain unsupported. Dynamic prefixes followed by dotted keys and
ancestor evaluation requiring intervening static prefixes are also explicitly
rejected; a static prefix is supported when it is the compiled rule path.
Static string leaf declarations are seeded as empty sets once before body
evaluation, without overwriting accumulated members. Key and value output
expressions share index bindings; named indices pair entries, while independent
wildcards retain Cartesian-product semantics. Output-loop and binding metadata
is selected by body query as well as module and expression, so differing body
bindings cannot overwrite each other's head plans. Both execution modes
accumulate all independent partial-rule bodies, while `else` bodies retain
first-success semantics. Static child references and entrypoints include
dynamic keyed-set definitions indexed once per parent and merged only when a
child is looked up. A known constant child key is bound and checked before
unrelated body statements, avoiding false recursion through nonmatching
dynamic definitions. The compiler also filters those definitions by the
requested key before set insertion, rather than calling the entire parent
and introducing cycles through sibling rules.
Both evaluators check ready input/reference keys before body evaluation,
between hoisted loops, and before output evaluation. Early filters defer
declared-but-undefined locals until binding and run after hoisted assignments
as well as ordinary statements. Scalar key bindings using `:=` or `=` (in
either operand order for `=`) can precede unrelated dependent statements.
Calls, comprehensions, and unresolved references conservatively defer early
filtering. Both evaluators always check the actual output key before
evaluating its value or accumulating a projected contribution, preventing
sibling writes even when early filtering is deferred. Interpreter recursion
tracking distinguishes parent and child projections of the same definition,
while repeated evaluation of the same projection remains a cycle.
Programs containing child projections enable runtime recursion checking in
both VM modes; genuine cycles propagate an error rather than becoming
undefined results.
- `ObjectCreateParams` reuses arrays of literal key/value pairs and register
pairs. Literal keys must be sorted to match template order.
- `ArrayCreateParams` and `SetCreateParams` store register lists. The VM checks
Expand Down Expand Up @@ -304,4 +347,3 @@ artifacts. Some contain `Vec` fields; the compiler is responsible for ensuring
indices remain valid and stable across serialization boundaries.

---

60 changes: 59 additions & 1 deletion src/compiled_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ use crate::scheduler::*;
use crate::utils::*;
use crate::*;

use alloc::collections::BTreeMap;
use alloc::borrow::Cow;
use alloc::collections::{BTreeMap, BTreeSet};
use anyhow::Result;

#[cfg(feature = "azure_policy")]
Expand Down Expand Up @@ -216,6 +217,9 @@ pub(crate) struct CompiledPolicyData {
pub(crate) parser_contexts: Rc<Vec<ParserContext>>,
pub(crate) schedule: Option<Rc<Schedule>>,
pub(crate) rules: Map<String, Vec<Ref<Rule>>>,
// Dynamic keyed-set definitions are indexed once per parent for child lookups.
pub(crate) keyed_set_parent_rules: Map<String, KeyedSetParentRules>,
pub(crate) keyed_set_child_parents: Map<String, String>,
pub(crate) default_rules: Map<String, Vec<DefaultRuleInfo>>,
pub(crate) rule_path_components: Map<String, Vec<String>>,
pub(crate) imports: BTreeMap<String, Ref<Expr>>,
Expand All @@ -241,3 +245,57 @@ pub(crate) struct CompiledPolicyData {
// Pre-computed loop hoisting information
pub(crate) loop_hoisting_table: HoistedLoopsLookup,
}

#[derive(Debug, Clone, Default)]
pub(crate) struct KeyedSetParentRules {
pub(crate) definitions: Vec<Ref<Rule>>,
rule_set: BTreeSet<Ref<Rule>>,
}

impl KeyedSetParentRules {
pub(crate) fn insert(&mut self, rule: Ref<Rule>) {
if self.rule_set.insert(rule.clone()) {
self.definitions.push(rule);
}
}

pub(crate) fn contains(&self, rule: &Ref<Rule>) -> bool {
self.rule_set.contains(rule)
}
}

impl CompiledPolicyData {
pub(crate) fn rules_for_lookup(&self, path: &str) -> Option<Cow<'_, [Ref<Rule>]>> {
let static_rules = self.rules.get(path)?;
let Some(parent) = self.keyed_set_child_parents.get(path) else {
return Some(Cow::Borrowed(static_rules));
};
let Some(dynamic_rules) = self.keyed_set_parent_rules.get(parent) else {
return Some(Cow::Borrowed(static_rules));
};

let mut rules = static_rules.clone();
rules.extend(dynamic_rules.definitions.iter().cloned());
Some(Cow::Owned(rules))
}

pub(crate) fn keyed_set_child_filter(&self, path: &str, rule: &Ref<Rule>) -> Option<Value> {
let parent = self.keyed_set_child_parents.get(path)?;
if !self.keyed_set_parent_rules.get(parent)?.contains(rule) {
return None;
}
self.rule_path_components
.get(path)?
.last()
.map(|key| Value::from(key.clone()))
}

pub(crate) fn is_projected_keyed_set_rule(&self, path: &str, rule: &Ref<Rule>) -> bool {
let Some(parent) = self.keyed_set_child_parents.get(path) else {
return false;
};
self.keyed_set_parent_rules
.get(parent)
.is_some_and(|rules| rules.contains(rule))
}
}
Loading
Loading