feat(rvm): implement Azure Policy condition evaluation (#661)

Add VM support for Azure Policy's condition operators and allOf/anyOf
short-circuit logic, gated behind cfg(feature = "azure_policy").

Policy conditions (equals, contains, like, match, exists, and their
negations — 21 total) are encoded as a single PolicyCondition
instruction with a PolicyOp sub-opcode rather than bloating the
Instruction enum with 21 variants. The dispatch handles Azure Policy's
quirky comparison semantics: case-insensitive string comparison,
string↔number coercion, null vs undefined distinction, and element-wise
collection membership.

allOf/anyOf blocks use four instructions — LogicalBlockStart,
AllOfNext/AnyOfNext, and LogicalBlockEnd — that wire up a result
register and short-circuit on the first failing (allOf) or passing
(anyOf) child.

Helper functions for case-folded comparison, wildcard/glob matching, and
type coercion live in builtins::azure_policy::helpers.

Two YAML test suites (~2200 lines) exercise the full operator matrix and
the allOf/anyOf control flow.
This commit is contained in:
Anand Krishnamoorthi
2026-04-07 19:04:24 -05:00
committed by GitHub
parent 83ce8c3580
commit 4d35744c4f
10 changed files with 3165 additions and 3 deletions

View File

@@ -47,6 +47,156 @@ pub fn as_str(value: &Value) -> Option<&str> {
} }
} }
/// Coerce a value to its string representation for policy comparison operators.
///
/// Azure Policy coerces numbers and booleans to strings when used with string
/// operators (`like`, `match`, `contains`, `matchInsensitively`). This is
/// needed when, for example, a count result (always a number) is compared
/// using a string operator: `count(...) like 2`.
pub fn coerce_to_string(value: &Value) -> Option<String> {
match *value {
Value::String(ref s) => Some(s.to_string()),
Value::Number(ref n) => Some(n.format_decimal()),
Value::Bool(b) => Some(if b { "true" } else { "false" }.to_string()),
_ => None,
}
}
pub fn coerce_to_string_ci(value: &Value) -> Option<String> {
coerce_to_string(value).map(|s| strings::case_fold::fold(&s).into_owned())
}
// ── Collection helpers ────────────────────────────────────────────────
/// Check if an array or set contains a null sentinel.
pub fn collection_has_null(v: &Value) -> bool {
match *v {
Value::Array(ref items) => items.iter().any(|i| matches!(i, Value::Null)),
Value::Set(ref items) => items.iter().any(|i| matches!(i, Value::Null)),
_ => false,
}
}
/// Check if any non-null element in a collection case-insensitively equals `target`.
/// Scalar RHS is treated as a single-element collection.
pub fn collection_any_ci_eq_excluding_null(collection: &Value, target: &Value) -> bool {
match *collection {
Value::Array(ref items) => items
.iter()
.filter(|i| !matches!(i, Value::Null))
.any(|i| case_insensitive_equals(i, target)),
Value::Set(ref items) => items
.iter()
.filter(|i| !matches!(i, Value::Null))
.any(|i| case_insensitive_equals(i, target)),
_ => case_insensitive_equals(collection, target),
}
}
pub fn as_boolish(value: &Value) -> Option<bool> {
match *value {
Value::Bool(b) => Some(b),
Value::String(ref s) => {
if s.eq_ignore_ascii_case("true") {
Some(true)
} else if s.eq_ignore_ascii_case("false") {
Some(false)
} else {
None
}
}
_ => None,
}
}
// ── Comparison and coercion ───────────────────────────────────────────
pub fn compare_values(left: &Value, right: &Value) -> Option<i8> {
if is_undefined(left) || is_undefined(right) {
return None;
}
#[allow(clippy::pattern_type_mismatch)]
match (left, right) {
(Value::String(a), Value::String(b)) => Some(match strings::case_fold::cmp(a, b) {
core::cmp::Ordering::Less => -1,
core::cmp::Ordering::Equal => 0,
core::cmp::Ordering::Greater => 1,
}),
(Value::Number(a), Value::Number(b)) => Some(if a < b {
-1
} else if a > b {
1
} else {
0
}),
(Value::Bool(a), Value::Bool(b)) => Some(if a == b {
0
} else if !a && *b {
-1
} else {
1
}),
// String ↔ Number coercion
(Value::String(s), Value::Number(n)) => try_coerce_to_number(s).map(|sn| {
if &sn < n {
-1
} else if &sn > n {
1
} else {
0
}
}),
(Value::Number(n), Value::String(s)) => try_coerce_to_number(s).map(|sn| {
if n < &sn {
-1
} else if n > &sn {
1
} else {
0
}
}),
_ => None,
}
}
pub fn case_insensitive_equals(left: &Value, right: &Value) -> bool {
if is_undefined(left) || is_undefined(right) {
return false;
}
// Azure Policy treats an explicit null field value as "" (empty string)
// for comparison purposes. Missing fields are Undefined and caught above.
#[allow(clippy::pattern_type_mismatch)]
match (left, right) {
(Value::Null, Value::Null) => true,
(Value::Null, Value::String(b)) => strings::case_fold::eq("", b),
(Value::String(a), Value::Null) => strings::case_fold::eq(a, ""),
(Value::String(a), Value::String(b)) => strings::case_fold::eq(a, b),
// String ↔ Number coercion
(Value::String(s), Value::Number(_)) | (Value::Number(_), Value::String(s)) => {
try_coerce_to_number(s).is_some_and(|n| {
let num_val = Value::Number(n);
let other = if matches!(left, Value::String(_)) {
right
} else {
left
};
&num_val == other
})
}
// String ↔ Bool coercion ("true"/"false" ↔ true/false)
(Value::String(_), Value::Bool(b)) | (Value::Bool(b), Value::String(_)) => {
as_boolish(if matches!(left, Value::String(_)) {
left
} else {
right
}) == Some(*b)
}
_ => left == right,
}
}
/// Try to parse a string as a number for Azure Policy type coercion. /// Try to parse a string as a number for Azure Policy type coercion.
pub fn try_coerce_to_number(s: &str) -> Option<crate::number::Number> { pub fn try_coerce_to_number(s: &str) -> Option<crate::number::Number> {
use core::str::FromStr as _; use core::str::FromStr as _;
@@ -61,6 +211,103 @@ pub fn try_coerce_to_number(s: &str) -> Option<crate::number::Number> {
}) })
} }
// ── Pattern matching ──────────────────────────────────────────────────
pub fn match_pattern(input_val: &Value, pattern_val: &Value, insensitive: bool) -> bool {
let Some(mut input) = coerce_to_string(input_val) else {
return false;
};
let Some(mut pattern) = coerce_to_string(pattern_val) else {
return false;
};
if insensitive {
input = strings::case_fold::fold(&input).into_owned();
pattern = strings::case_fold::fold(&pattern).into_owned();
}
match_question_hash_pattern(&input, &pattern)
}
pub fn match_like_pattern_ci(input: &str, pattern: &str) -> bool {
wildcard_match(input, pattern)
}
fn next_char(s: &str, index: usize) -> Option<(char, usize)> {
s.get(index..)?
.chars()
.next()
.map(|ch| (ch, index.saturating_add(ch.len_utf8())))
}
fn wildcard_match(input: &str, pattern: &str) -> bool {
let (mut ii, mut pi) = (0_usize, 0_usize);
let mut star_pat: Option<usize> = None;
let mut star_inp = 0_usize;
while ii < input.len() {
let pat = next_char(pattern, pi);
let inp = next_char(input, ii);
if let (Some((pc, next_pi)), Some((ic, next_ii))) = (pat, inp) {
if pc == '?' || pc == ic {
pi = next_pi;
ii = next_ii;
continue;
}
}
if matches!(pat, Some(('*', _))) {
star_pat = Some(pi);
star_inp = ii;
pi = pi.saturating_add('*'.len_utf8());
} else if let Some(saved_pi) = star_pat {
pi = saved_pi.saturating_add('*'.len_utf8());
if let Some((_, next_ii)) = next_char(input, star_inp) {
star_inp = next_ii;
ii = star_inp;
} else {
return false;
}
} else {
return false;
}
}
while matches!(next_char(pattern, pi), Some(('*', _))) {
pi = pi.saturating_add('*'.len_utf8());
}
pi == pattern.len()
}
pub fn match_question_hash_pattern(input: &str, pattern: &str) -> bool {
let mut input_chars = input.chars();
let mut pattern_chars = pattern.chars();
loop {
match (input_chars.next(), pattern_chars.next()) {
(None, None) => return true,
(Some(_), None) | (None, Some(_)) => return false,
(Some(input_char), Some(pattern_char)) => {
if pattern_char == '.' {
// '.' matches any single character (letter, digit, or special).
} else if pattern_char == '#' {
if !input_char.is_ascii_digit() {
return false;
}
} else if pattern_char == '?' {
if !input_char.is_ascii_alphabetic() {
return false;
}
} else if input_char != pattern_char {
return false;
}
}
}
}
}
// ── Path resolution ─────────────────────────────────────────────────── // ── Path resolution ───────────────────────────────────────────────────
pub fn resolve_path(root: &Value, path: &str) -> Value { pub fn resolve_path(root: &Value, path: &str) -> Value {

View File

@@ -5,7 +5,7 @@ use alloc::format;
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use super::types::GuardMode; use super::types::{GuardMode, LogicalBlockMode, PolicyOp};
use super::{Instruction, InstructionData, LiteralOrRegister}; use super::{Instruction, InstructionData, LiteralOrRegister};
impl Instruction { impl Instruction {
@@ -291,6 +291,51 @@ impl core::fmt::Display for Instruction {
|k| format!("COMPREHENSION_YIELD R({}) R({})", k, value_reg), |k| format!("COMPREHENSION_YIELD R({}) R({})", k, value_reg),
), ),
Instruction::ComprehensionEnd {} => String::from("COMPREHENSION_END"), Instruction::ComprehensionEnd {} => String::from("COMPREHENSION_END"),
// Azure Policy consolidated instruction
Instruction::PolicyCondition {
dest,
left,
right,
op,
} => match op {
PolicyOp::Not => format!("{} R({}) R({})", op.display_name(), dest, left),
_ => format!("{} R({}) R({}) R({})", op.display_name(), dest, left, right),
},
// AllOf / AnyOf structured instructions
Instruction::LogicalBlockStart {
mode,
result,
end_pc,
} => {
let name = match mode {
LogicalBlockMode::AllOf => "ALL_OF_START",
LogicalBlockMode::AnyOf => "ANY_OF_START",
};
format!("{} R({}) {}", name, result, end_pc)
}
Instruction::AllOfNext {
check,
result,
end_pc,
} => {
format!("ALL_OF_NEXT R({}) R({}) {}", check, result, end_pc)
}
Instruction::AnyOfNext {
check,
result,
end_pc,
} => {
format!("ANY_OF_NEXT R({}) R({}) {}", check, result, end_pc)
}
Instruction::LogicalBlockEnd { mode, result } => {
let name = match mode {
LogicalBlockMode::AllOf => "ALL_OF_END",
LogicalBlockMode::AnyOf => "ANY_OF_END",
};
format!("{} R({})", name, result)
}
}; };
write!(f, "{}", text) write!(f, "{}", text)
} }

View File

@@ -10,7 +10,9 @@ pub use params::{
FunctionCallParams, InstructionData, LoopStartParams, ObjectCreateParams, SetCreateParams, FunctionCallParams, InstructionData, LoopStartParams, ObjectCreateParams, SetCreateParams,
VirtualDataDocumentLookupParams, VirtualDataDocumentLookupParams,
}; };
pub use types::{ComprehensionMode, GuardMode, LiteralOrRegister, LoopMode}; pub use types::{
ComprehensionMode, GuardMode, LiteralOrRegister, LogicalBlockMode, LoopMode, PolicyOp,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -355,6 +357,65 @@ pub enum Instruction {
/// End a comprehension block /// End a comprehension block
ComprehensionEnd {}, ComprehensionEnd {},
// ── Azure Policy condition operators (consolidated) ────────────────
/// Consolidated Azure Policy condition instruction.
///
/// Replaces 21 separate Policy* variants. The `op` discriminant selects
/// the specific Azure Policy condition semantics.
///
/// For most ops: `dest = op(left, right)`.
/// For `PolicyOp::Not`: `dest = !is_true(left)`, `right` is unused (0).
/// For `PolicyOp::ValueConditionGuard`: `left` = value register,
/// `right` = condition register.
PolicyCondition {
dest: u8,
left: u8,
right: u8,
op: PolicyOp,
},
// ── AllOf / AnyOf structured short-circuit instructions ───────────
/// Initialize allOf/anyOf: set result register to false.
LogicalBlockStart {
mode: LogicalBlockMode,
/// Register that accumulates the result.
result: u8,
/// PC of the corresponding End instruction.
end_pc: u16,
},
/// Check one allOf child: if not true, short-circuit (result stays false),
/// jump to end_pc.
AllOfNext {
/// Register holding the child condition result.
check: u8,
/// Register that accumulates the allOf result.
result: u8,
/// PC of the AllOfEnd instruction (jump target on short-circuit).
end_pc: u16,
},
/// Check one anyOf child: if true, short-circuit (set result to true),
/// jump to end_pc.
AnyOfNext {
/// Register holding the child condition result.
check: u8,
/// Register that accumulates the anyOf result.
result: u8,
/// PC of the AnyOfEnd instruction.
end_pc: u16,
},
/// Finalize allOf/anyOf block.
///
/// For AllOf: all children passed → set result to true.
/// For AnyOf: no child matched → result stays false (no-op).
LogicalBlockEnd {
mode: LogicalBlockMode,
/// Register that accumulates the result.
result: u8,
},
} }
impl Instruction { impl Instruction {

View File

@@ -46,6 +46,110 @@ pub enum ComprehensionMode {
Object, Object,
} }
/// Azure Policy condition operator sub-opcodes.
///
/// Each variant maps to one of the ~21 Azure Policy condition operators.
/// Stored inside `Instruction::PolicyCondition` to collapse 21 enum variants
/// into a single instruction with a sub-op discriminant.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PolicyOp {
Equals,
NotEquals,
Greater,
GreaterOrEquals,
Less,
LessOrEquals,
In,
NotIn,
Contains,
NotContains,
ContainsKey,
NotContainsKey,
Like,
NotLike,
Match,
NotMatch,
MatchInsensitively,
NotMatchInsensitively,
Exists,
/// Guard for `value:` conditions — forces false when LHS is undefined.
/// Uses `left` = value register, `right` = condition register.
ValueConditionGuard,
/// Logical negation: `!is_true(operand)`. Uses `left` = operand, `right` is unused (0).
Not,
}
impl PolicyOp {
/// Display name used in assembly listings and Debug output.
pub const fn display_name(self) -> &'static str {
match self {
Self::Equals => "POLICY_EQUALS",
Self::NotEquals => "POLICY_NOT_EQUALS",
Self::Greater => "POLICY_GREATER",
Self::GreaterOrEquals => "POLICY_GREATER_OR_EQUALS",
Self::Less => "POLICY_LESS",
Self::LessOrEquals => "POLICY_LESS_OR_EQUALS",
Self::In => "POLICY_IN",
Self::NotIn => "POLICY_NOT_IN",
Self::Contains => "POLICY_CONTAINS",
Self::NotContains => "POLICY_NOT_CONTAINS",
Self::ContainsKey => "POLICY_CONTAINS_KEY",
Self::NotContainsKey => "POLICY_NOT_CONTAINS_KEY",
Self::Like => "POLICY_LIKE",
Self::NotLike => "POLICY_NOT_LIKE",
Self::Match => "POLICY_MATCH",
Self::NotMatch => "POLICY_NOT_MATCH",
Self::MatchInsensitively => "POLICY_MATCH_INSENSITIVELY",
Self::NotMatchInsensitively => "POLICY_NOT_MATCH_INSENSITIVELY",
Self::Exists => "POLICY_EXISTS",
Self::ValueConditionGuard => "VALUE_CONDITION_GUARD",
Self::Not => "POLICY_NOT",
}
}
/// Compact name for tabular assembly listings.
pub const fn compact_name(self) -> &'static str {
match self {
Self::Equals => "POLICY_EQ",
Self::NotEquals => "POLICY_NE",
Self::Greater => "POLICY_GT",
Self::GreaterOrEquals => "POLICY_GE",
Self::Less => "POLICY_LT",
Self::LessOrEquals => "POLICY_LE",
Self::In => "POLICY_IN",
Self::NotIn => "POLICY_NOT_IN",
Self::Contains => "POLICY_CONTAINS",
Self::NotContains => "POLICY_NOT_CONTAINS",
Self::ContainsKey => "POLICY_CONTAINS_KEY",
Self::NotContainsKey => "POLICY_NOT_CONTAINS_KEY",
Self::Like => "POLICY_LIKE",
Self::NotLike => "POLICY_NOT_LIKE",
Self::Match => "POLICY_MATCH",
Self::NotMatch => "POLICY_NOT_MATCH",
Self::MatchInsensitively => "POLICY_MATCH_CI",
Self::NotMatchInsensitively => "POLICY_NOT_MATCH_CI",
Self::Exists => "POLICY_EXISTS",
Self::ValueConditionGuard => "VAL_COND_GUARD",
Self::Not => "POLICY_NOT",
}
}
/// Returns `true` for negated condition operators (NotEquals, NotIn, etc.).
pub const fn is_negated(self) -> bool {
matches!(
self,
Self::NotEquals
| Self::NotIn
| Self::NotContains
| Self::NotContainsKey
| Self::NotLike
| Self::NotMatch
| Self::NotMatchInsensitively
)
}
}
/// Guard sub-modes for the consolidated `Guard` instruction. /// Guard sub-modes for the consolidated `Guard` instruction.
#[repr(u8)] #[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -57,3 +161,11 @@ pub enum GuardMode {
/// Assert not undefined — fail (return undefined) if register is undefined. /// Assert not undefined — fail (return undefined) if register is undefined.
NotUndefined, NotUndefined,
} }
/// Mode discriminant for merged AllOf/AnyOf Start and End instructions.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LogicalBlockMode {
AllOf,
AnyOf,
}

View File

@@ -911,6 +911,15 @@ fn format_instruction_readable(
let base = format!("{}}} CompEnd", indent); let base = format!("{}}} CompEnd", indent);
align_comment(&base, "End comprehension block", config.comment_column) align_comment(&base, "End comprehension block", config.comment_column)
} }
// Azure Policy & allOf/anyOf instructions — use Display impl
instruction @ Instruction::PolicyCondition { .. }
| instruction @ Instruction::LogicalBlockStart { .. }
| instruction @ Instruction::AllOfNext { .. }
| instruction @ Instruction::AnyOfNext { .. }
| instruction @ Instruction::LogicalBlockEnd { .. } => {
format!("{}{}", indent, instruction)
}
} }
} }
@@ -1045,6 +1054,19 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
Instruction::ComprehensionBegin { .. } => "COMP_BEGIN", Instruction::ComprehensionBegin { .. } => "COMP_BEGIN",
Instruction::ComprehensionYield { .. } => "COMP_YIELD", Instruction::ComprehensionYield { .. } => "COMP_YIELD",
Instruction::ComprehensionEnd {} => "COMP_END", Instruction::ComprehensionEnd {} => "COMP_END",
// Azure Policy instructions
Instruction::PolicyCondition { op, .. } => op.compact_name(),
// AllOf / AnyOf
Instruction::LogicalBlockStart { mode, .. } => match mode {
crate::rvm::instructions::LogicalBlockMode::AllOf => "ALL_OF_START",
crate::rvm::instructions::LogicalBlockMode::AnyOf => "ANY_OF_START",
},
Instruction::AllOfNext { .. } => "ALL_OF_NEXT",
Instruction::AnyOfNext { .. } => "ANY_OF_NEXT",
Instruction::LogicalBlockEnd { mode, .. } => match mode {
crate::rvm::instructions::LogicalBlockMode::AllOf => "ALL_OF_END",
crate::rvm::instructions::LogicalBlockMode::AnyOf => "ANY_OF_END",
},
} }
} }

View File

@@ -9,7 +9,7 @@
clippy::pattern_type_mismatch clippy::pattern_type_mismatch
)] // tests unwrap conversions and slice math for brevity )] // tests unwrap conversions and slice math for brevity
use crate::rvm::instructions::{GuardMode, Instruction, LoopMode}; use crate::rvm::instructions::{GuardMode, Instruction, LogicalBlockMode, LoopMode, PolicyOp};
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use alloc::vec::Vec; use alloc::vec::Vec;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
@@ -83,6 +83,41 @@ pub fn parse_instruction(text: &str) -> Result<Instruction> {
"ComprehensionYield" => parse_comprehension_add(params_text), "ComprehensionYield" => parse_comprehension_add(params_text),
"ReturnUndefinedIfNotTrue" => parse_return_undefined_if_not_true(params_text), "ReturnUndefinedIfNotTrue" => parse_return_undefined_if_not_true(params_text),
"CoalesceUndefinedToNull" => parse_coalesce_undefined_to_null(params_text), "CoalesceUndefinedToNull" => parse_coalesce_undefined_to_null(params_text),
// Azure Policy condition operators
"PolicyEquals" => parse_policy_condition(params_text, PolicyOp::Equals),
"PolicyNotEquals" => parse_policy_condition(params_text, PolicyOp::NotEquals),
"PolicyGreater" => parse_policy_condition(params_text, PolicyOp::Greater),
"PolicyGreaterOrEquals" => {
parse_policy_condition(params_text, PolicyOp::GreaterOrEquals)
}
"PolicyLess" => parse_policy_condition(params_text, PolicyOp::Less),
"PolicyLessOrEquals" => parse_policy_condition(params_text, PolicyOp::LessOrEquals),
"PolicyIn" => parse_policy_condition(params_text, PolicyOp::In),
"PolicyNotIn" => parse_policy_condition(params_text, PolicyOp::NotIn),
"PolicyContains" => parse_policy_condition(params_text, PolicyOp::Contains),
"PolicyNotContains" => parse_policy_condition(params_text, PolicyOp::NotContains),
"PolicyContainsKey" => parse_policy_condition(params_text, PolicyOp::ContainsKey),
"PolicyNotContainsKey" => parse_policy_condition(params_text, PolicyOp::NotContainsKey),
"PolicyLike" => parse_policy_condition(params_text, PolicyOp::Like),
"PolicyNotLike" => parse_policy_condition(params_text, PolicyOp::NotLike),
"PolicyMatch" => parse_policy_condition(params_text, PolicyOp::Match),
"PolicyNotMatch" => parse_policy_condition(params_text, PolicyOp::NotMatch),
"PolicyMatchInsensitively" => {
parse_policy_condition(params_text, PolicyOp::MatchInsensitively)
}
"PolicyNotMatchInsensitively" => {
parse_policy_condition(params_text, PolicyOp::NotMatchInsensitively)
}
"PolicyExists" => parse_policy_condition(params_text, PolicyOp::Exists),
"ValueConditionGuard" => parse_value_condition_guard(params_text),
"PolicyNot" => parse_policy_not(params_text),
// AllOf / AnyOf
"AllOfStart" => parse_logical_block_start(params_text, LogicalBlockMode::AllOf),
"AllOfNext" => parse_allof_next(params_text),
"AllOfEnd" => parse_logical_block_end(params_text, LogicalBlockMode::AllOf),
"AnyOfStart" => parse_logical_block_start(params_text, LogicalBlockMode::AnyOf),
"AnyOfNext" => parse_anyof_next(params_text),
"AnyOfEnd" => parse_logical_block_end(params_text, LogicalBlockMode::AnyOf),
_ => bail!("Unknown instruction: {}", name), _ => bail!("Unknown instruction: {}", name),
} }
} else { } else {
@@ -707,3 +742,83 @@ fn parse_coalesce_undefined_to_null(params_text: &str) -> Result<Instruction> {
register: register.try_into().unwrap(), register: register.try_into().unwrap(),
}) })
} }
/// Generic parser for PolicyCondition instructions with { dest, left, right } fields.
fn parse_policy_condition(params_text: &str, op: PolicyOp) -> Result<Instruction> {
let params = parse_params(params_text)?;
let dest: u8 = get_param_u16(&params, "dest")?.try_into().unwrap();
let left: u8 = get_param_u16(&params, "left")?.try_into().unwrap();
let right: u8 = get_param_u16(&params, "right")?.try_into().unwrap();
Ok(Instruction::PolicyCondition {
dest,
left,
right,
op,
})
}
fn parse_value_condition_guard(params_text: &str) -> Result<Instruction> {
let params = parse_params(params_text)?;
let dest: u8 = get_param_u16(&params, "dest")?.try_into().unwrap();
let value: u8 = get_param_u16(&params, "value")?.try_into().unwrap();
let condition: u8 = get_param_u16(&params, "condition")?.try_into().unwrap();
Ok(Instruction::PolicyCondition {
dest,
left: value,
right: condition,
op: PolicyOp::ValueConditionGuard,
})
}
fn parse_policy_not(params_text: &str) -> Result<Instruction> {
let params = parse_params(params_text)?;
let dest: u8 = get_param_u16(&params, "dest")?.try_into().unwrap();
let operand: u8 = get_param_u16(&params, "operand")?.try_into().unwrap();
Ok(Instruction::PolicyCondition {
dest,
left: operand,
right: 0,
op: PolicyOp::Not,
})
}
fn parse_logical_block_start(params_text: &str, mode: LogicalBlockMode) -> Result<Instruction> {
let params = parse_params(params_text)?;
let result: u8 = get_param_u16(&params, "result")?.try_into().unwrap();
let end_pc = get_param_u16(&params, "end_pc")?;
Ok(Instruction::LogicalBlockStart {
mode,
result,
end_pc,
})
}
fn parse_allof_next(params_text: &str) -> Result<Instruction> {
let params = parse_params(params_text)?;
let check: u8 = get_param_u16(&params, "check")?.try_into().unwrap();
let result: u8 = get_param_u16(&params, "result")?.try_into().unwrap();
let end_pc = get_param_u16(&params, "end_pc")?;
Ok(Instruction::AllOfNext {
check,
result,
end_pc,
})
}
fn parse_logical_block_end(params_text: &str, mode: LogicalBlockMode) -> Result<Instruction> {
let params = parse_params(params_text)?;
let result: u8 = get_param_u16(&params, "result")?.try_into().unwrap();
Ok(Instruction::LogicalBlockEnd { mode, result })
}
fn parse_anyof_next(params_text: &str) -> Result<Instruction> {
let params = parse_params(params_text)?;
let check: u8 = get_param_u16(&params, "check")?.try_into().unwrap();
let result: u8 = get_param_u16(&params, "result")?.try_into().unwrap();
let end_pc = get_param_u16(&params, "end_pc")?;
Ok(Instruction::AnyOfNext {
check,
result,
end_pc,
})
}

View File

@@ -1059,4 +1059,10 @@ mod tests {
fn run_loop_test_file(file: &str) { fn run_loop_test_file(file: &str) {
run_vm_test_suite(file).unwrap() run_vm_test_suite(file).unwrap()
} }
#[cfg(feature = "azure_policy")]
#[test_resources("tests/rvm/vm/suites/azure_policy/*.yaml")]
fn run_azure_policy_test_file(file: &str) {
run_vm_test_suite(file).unwrap()
}
} }

View File

@@ -807,6 +807,297 @@ impl RegoVM {
let result = self.get_register(0)?.clone(); let result = self.get_register(0)?.clone();
Ok(InstructionOutcome::Return(result)) Ok(InstructionOutcome::Return(result))
} }
other => self.execute_policy_instruction(program, other),
}
}
#[cfg(not(feature = "azure_policy"))]
fn execute_policy_instruction(
&mut self,
program: &Program,
instruction: Instruction,
) -> Result<InstructionOutcome> {
match instruction {
instruction @ (Instruction::PolicyCondition { .. }
| Instruction::LogicalBlockStart { .. }
| Instruction::LogicalBlockEnd { .. }
| Instruction::AllOfNext { .. }
| Instruction::AnyOfNext { .. }) => Err(VmError::UnhandledInstruction {
instruction: alloc::format!("{:?} requires the azure_policy feature", instruction),
pc: self.pc,
}),
other => self.execute_virtual_instruction(program, other),
}
}
/// Check whether `l` "contains" `r` using Azure Policy semantics.
///
/// Works on strings (case-insensitive substring), arrays/sets (element
/// membership), and objects (key membership). For string haystacks,
/// non-string scalar RHS values are coerced to strings before the
/// substring check. For non-string scalar LHS values, coercion to string
/// only happens when the RHS is already a string.
#[cfg(feature = "azure_policy")]
#[inline]
fn policy_contains_check(l: &Value, r: &Value) -> bool {
use crate::builtins::azure_policy::helpers::{case_insensitive_equals, coerce_to_string};
use crate::languages::azure_policy::strings;
match *l {
Value::String(ref haystack) => match *r {
Value::String(ref needle) => strings::case_fold::contains(haystack, needle),
_ => coerce_to_string(r)
.is_some_and(|needle| strings::case_fold::contains(haystack, &needle)),
},
Value::Array(ref items) => items.iter().any(|item| case_insensitive_equals(item, r)),
Value::Set(ref items) => items.iter().any(|item| case_insensitive_equals(item, r)),
// ARM template contains(object, key) checks key membership.
Value::Object(ref map) => map.keys().any(|key| case_insensitive_equals(key, r)),
// Coerce non-string scalar LHS (e.g., count result)
// to a string only when the RHS is already a string.
_ => {
if let Value::String(ref needle) = *r {
coerce_to_string(l)
.is_some_and(|haystack| strings::case_fold::contains(&haystack, needle))
} else {
false
}
}
}
}
/// Evaluate a Policy comparison operator. Undefined LHS → false.
#[cfg(feature = "azure_policy")]
fn policy_compare(
&mut self,
dest: u8,
left: u8,
right: u8,
cmp: fn(i8) -> bool,
) -> Result<InstructionOutcome> {
use crate::builtins::azure_policy::helpers::{compare_values, is_undefined};
let l = self.get_register(left)?;
if is_undefined(l) {
self.set_register(dest, Value::Bool(false))?;
} else {
let r = self.get_register(right)?;
let result = compare_values(l, r).is_some_and(cmp);
self.set_register(dest, Value::Bool(result))?;
}
Ok(InstructionOutcome::Continue)
}
#[cfg(feature = "azure_policy")]
fn execute_policy_instruction(
&mut self,
program: &Program,
instruction: Instruction,
) -> Result<InstructionOutcome> {
use crate::builtins::azure_policy::helpers::{
as_boolish, case_insensitive_equals, coerce_to_string_ci,
collection_any_ci_eq_excluding_null, collection_has_null, is_true, is_undefined,
match_like_pattern_ci, match_pattern,
};
use crate::rvm::instructions::{LogicalBlockMode, PolicyOp};
use Instruction::*;
match instruction {
PolicyCondition {
dest,
left,
right,
op,
} => {
let l = self.get_register(left)?;
let result = match op {
PolicyOp::Equals => {
let r = self.get_register(right)?;
if is_undefined(l) {
matches!(r, Value::Null)
} else {
case_insensitive_equals(l, r)
}
}
PolicyOp::NotEquals => {
let r = self.get_register(right)?;
if is_undefined(l) {
!matches!(r, Value::Null)
} else {
!case_insensitive_equals(l, r)
}
}
PolicyOp::Greater => {
return self.policy_compare(dest, left, right, |c| c > 0);
}
PolicyOp::GreaterOrEquals => {
return self.policy_compare(dest, left, right, |c| c >= 0);
}
PolicyOp::Less => {
return self.policy_compare(dest, left, right, |c| c < 0);
}
PolicyOp::LessOrEquals => {
return self.policy_compare(dest, left, right, |c| c <= 0);
}
PolicyOp::In => {
let r = self.get_register(right)?;
if is_undefined(l) {
collection_has_null(r)
} else if matches!(*l, Value::Null) || is_undefined(r) {
false
} else {
collection_any_ci_eq_excluding_null(r, l)
}
}
PolicyOp::NotIn => {
let r = self.get_register(right)?;
if is_undefined(l) {
!collection_has_null(r)
} else if matches!(*l, Value::Null) || is_undefined(r) {
true
} else {
!collection_any_ci_eq_excluding_null(r, l)
}
}
PolicyOp::Contains | PolicyOp::NotContains => {
let negated = op.is_negated();
if is_undefined(l) {
negated
} else {
let r = self.get_register(right)?;
if is_undefined(r) {
// undefined RHS: positive → false, negated → false
false
} else {
negated ^ Self::policy_contains_check(l, r)
}
}
}
PolicyOp::ContainsKey | PolicyOp::NotContainsKey => {
let negated = op.is_negated();
if is_undefined(l) {
negated
} else {
let r = self.get_register(right)?;
if is_undefined(r) {
false
} else {
let found = match *l {
Value::Object(ref map) => {
map.keys().any(|key| case_insensitive_equals(key, r))
}
_ => false,
};
negated ^ found
}
}
}
PolicyOp::Like | PolicyOp::NotLike => {
let negated = op.is_negated();
if is_undefined(l) {
negated
} else {
let r = self.get_register(right)?;
let positive = match (coerce_to_string_ci(l), coerce_to_string_ci(r)) {
(Some(input), Some(pattern)) => {
match_like_pattern_ci(&input, &pattern)
}
_ => false,
};
negated ^ positive
}
}
PolicyOp::Match
| PolicyOp::NotMatch
| PolicyOp::MatchInsensitively
| PolicyOp::NotMatchInsensitively => {
let negated = op.is_negated();
let case_insensitive = matches!(
op,
PolicyOp::MatchInsensitively | PolicyOp::NotMatchInsensitively
);
if is_undefined(l) {
negated
} else {
let r = self.get_register(right)?;
negated ^ match_pattern(l, r, case_insensitive)
}
}
PolicyOp::Exists => {
let r = self.get_register(right)?;
let expected = as_boolish(r).unwrap_or(false);
let is_defined = !is_undefined(l) && !matches!(l, Value::Null);
is_defined == expected
}
PolicyOp::ValueConditionGuard => {
// left = value register, right = condition register
if is_undefined(l) {
self.set_register(dest, Value::Bool(false))?;
return Ok(InstructionOutcome::Continue);
} else {
let c = self.get_register(right)?.clone();
self.set_register(dest, c)?;
return Ok(InstructionOutcome::Continue);
}
}
PolicyOp::Not => {
// left = operand, right unused
!is_true(l)
}
};
self.set_register(dest, Value::Bool(result))?;
Ok(InstructionOutcome::Continue)
}
// AllOf / AnyOf structured instructions
LogicalBlockStart {
mode: _,
result,
end_pc: _,
} => {
// Initialize result to false (pessimistic).
self.set_register(result, Value::Bool(false))?;
Ok(InstructionOutcome::Continue)
}
AllOfNext {
check,
result,
end_pc,
} => {
let val = self.get_register(check)?;
if !matches!(val, Value::Bool(true)) {
// Child failed — short-circuit. Ensure the block result is false.
self.set_register(result, Value::Bool(false))?;
self.pc = usize::from(end_pc);
}
Ok(InstructionOutcome::Continue)
}
AnyOfNext {
check,
result,
end_pc,
} => {
let val = self.get_register(check)?;
if matches!(val, Value::Bool(true)) {
// Child succeeded — short-circuit.
self.set_register(result, Value::Bool(true))?;
self.pc = usize::from(end_pc);
}
Ok(InstructionOutcome::Continue)
}
LogicalBlockEnd { mode, result } => {
match mode {
LogicalBlockMode::AllOf => {
// All children passed — set result to true.
self.set_register(result, Value::Bool(true))?;
}
LogicalBlockMode::AnyOf => {
// No child matched — result stays false (set by LogicalBlockStart).
}
}
Ok(InstructionOutcome::Continue)
}
other => self.execute_virtual_instruction(program, other), other => self.execute_virtual_instruction(program, other),
} }
} }

View File

@@ -0,0 +1,680 @@
# AllOf / AnyOf structured short-circuit instruction tests
#
# AllOf: conjunction with lazy short-circuit.
# AllOfStart — initialize result=false
# AllOfNext — if child is not true, set pc=end_pc; after the main-loop
# increment, execution resumes past AllOfEnd (result stays false)
# AllOfEnd — all children passed, set result=true
#
# AnyOf: disjunction with lazy short-circuit.
# AnyOfStart — initialize result=false
# AnyOfNext — if child is true, set result=true, set pc=end_pc; after the
# main-loop increment, execution resumes past AnyOfEnd
# AnyOfEnd — no child matched, result stays false
#
# PC semantics: end_pc points to the AllOfEnd/AnyOfEnd instruction.
# On short-circuit the VM sets self.pc = end_pc; because the main loop
# increments pc after Continue, the End instruction itself is skipped.
cases:
# =========================================================================
# AllOf basic cases
# =========================================================================
- note: allof_all_true
description: "allOf with two true children → true"
literals: []
instructions:
# PC 0: AllOfStart — end_pc=5 points to AllOfEnd at PC 5
- "AllOfStart { result: 0, end_pc: 5 }"
# Child 1: evaluate condition into r1
- "LoadTrue { dest: 1 }"
- "AllOfNext { check: 1, result: 0, end_pc: 5 }"
# Child 2: evaluate condition into r2
- "LoadTrue { dest: 2 }"
- "AllOfNext { check: 2, result: 0, end_pc: 5 }"
# PC 5: AllOfEnd — all passed
- "AllOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: true
- note: allof_first_false
description: "allOf with first child false → short-circuits, result false"
literals: []
instructions:
# PC 0: AllOfStart — end_pc=5
- "AllOfStart { result: 0, end_pc: 5 }"
# Child 1: false
- "LoadFalse { dest: 1 }"
- "AllOfNext { check: 1, result: 0, end_pc: 5 }"
# Child 2: this should be skipped
- "LoadTrue { dest: 2 }"
- "AllOfNext { check: 2, result: 0, end_pc: 5 }"
# PC 5: AllOfEnd
- "AllOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: false
- note: allof_second_false
description: "allOf with second child false → result false"
literals: []
instructions:
- "AllOfStart { result: 0, end_pc: 5 }"
- "LoadTrue { dest: 1 }"
- "AllOfNext { check: 1, result: 0, end_pc: 5 }"
- "LoadFalse { dest: 2 }"
- "AllOfNext { check: 2, result: 0, end_pc: 5 }"
- "AllOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: false
- note: allof_empty
description: "allOf with zero children → AllOfStart immediately followed by AllOfEnd → true"
literals: []
instructions:
# PC 0: AllOfStart — end_pc=1 points to AllOfEnd at PC 1
- "AllOfStart { result: 0, end_pc: 1 }"
# PC 1: AllOfEnd
- "AllOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: true
- note: allof_single_true
description: "allOf with single true child → true"
literals: []
instructions:
- "AllOfStart { result: 0, end_pc: 3 }"
- "LoadTrue { dest: 1 }"
- "AllOfNext { check: 1, result: 0, end_pc: 3 }"
- "AllOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: true
- note: allof_single_false
description: "allOf with single false child → false"
literals: []
instructions:
- "AllOfStart { result: 0, end_pc: 3 }"
- "LoadFalse { dest: 1 }"
- "AllOfNext { check: 1, result: 0, end_pc: 3 }"
- "AllOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: false
- note: allof_undefined_child
description: "allOf: first child undefined (not true), second child true → short-circuits to false"
literals: []
instructions:
# PC 0: AllOfEnd at PC 5
- "AllOfStart { result: 0, end_pc: 5 }"
# PC 1: r1 is undefined (not initialized) → AllOfNext short-circuits
- "AllOfNext { check: 1, result: 0, end_pc: 5 }"
# PC 2: Child 2 (skipped)
- "LoadTrue { dest: 2 }"
# PC 3 (skipped)
- "AllOfNext { check: 2, result: 0, end_pc: 5 }"
# PC 4: should not happen
- "LoadTrue { dest: 3 }"
# PC 5: AllOfEnd (skipped on short-circuit)
- "AllOfEnd { result: 0 }"
# PC 6
- "Return { value: 0 }"
want_result: false
# =========================================================================
# AnyOf basic cases
# =========================================================================
- note: anyof_first_true
description: "anyOf with first child true → short-circuits, result true"
literals: []
instructions:
# PC 0: AnyOfStart — end_pc=5
- "AnyOfStart { result: 0, end_pc: 5 }"
# Child 1: true
- "LoadTrue { dest: 1 }"
- "AnyOfNext { check: 1, result: 0, end_pc: 5 }"
# Child 2: should be skipped
- "LoadFalse { dest: 2 }"
- "AnyOfNext { check: 2, result: 0, end_pc: 5 }"
# PC 5: AnyOfEnd
- "AnyOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: true
- note: anyof_second_true
description: "anyOf with second child true → result true"
literals: []
instructions:
- "AnyOfStart { result: 0, end_pc: 5 }"
- "LoadFalse { dest: 1 }"
- "AnyOfNext { check: 1, result: 0, end_pc: 5 }"
- "LoadTrue { dest: 2 }"
- "AnyOfNext { check: 2, result: 0, end_pc: 5 }"
- "AnyOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: true
- note: anyof_all_false
description: "anyOf with all children false → result false"
literals: []
instructions:
- "AnyOfStart { result: 0, end_pc: 5 }"
- "LoadFalse { dest: 1 }"
- "AnyOfNext { check: 1, result: 0, end_pc: 5 }"
- "LoadFalse { dest: 2 }"
- "AnyOfNext { check: 2, result: 0, end_pc: 5 }"
- "AnyOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: false
- note: anyof_empty
description: "anyOf with zero children → false"
literals: []
instructions:
- "AnyOfStart { result: 0, end_pc: 1 }"
- "AnyOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: false
- note: anyof_single_true
literals: []
instructions:
- "AnyOfStart { result: 0, end_pc: 3 }"
- "LoadTrue { dest: 1 }"
- "AnyOfNext { check: 1, result: 0, end_pc: 3 }"
- "AnyOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: true
- note: anyof_single_false
literals: []
instructions:
- "AnyOfStart { result: 0, end_pc: 3 }"
- "LoadFalse { dest: 1 }"
- "AnyOfNext { check: 1, result: 0, end_pc: 3 }"
- "AnyOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: false
# =========================================================================
# Nested allOf inside anyOf
# =========================================================================
- note: nested_allof_in_anyof
description: |
anyOf [
allOf [false, true], -- fails
allOf [true, true] -- passes → anyOf result is true
]
literals: []
instructions:
# Outer anyOf: AnyOfEnd at PC 15
# PC 0
- "AnyOfStart { result: 0, end_pc: 15 }"
# --- anyOf child 1: allOf [false, true] ---
# Inner allOf #1: AllOfEnd at PC 6
# PC 1
- "AllOfStart { result: 3, end_pc: 6 }"
# PC 2
- "LoadFalse { dest: 1 }"
# PC 3: short-circuits → pc=6, +1→7 (skips AllOfEnd)
- "AllOfNext { check: 1, result: 3, end_pc: 6 }"
# PC 4 (skipped)
- "LoadTrue { dest: 2 }"
# PC 5 (skipped)
- "AllOfNext { check: 2, result: 3, end_pc: 6 }"
# PC 6
- "AllOfEnd { result: 3 }"
# PC 7: AnyOfNext — r3=false → no short-circuit, continue
- "AnyOfNext { check: 3, result: 0, end_pc: 15 }"
# --- anyOf child 2: allOf [true, true] ---
# Inner allOf #2: AllOfEnd at PC 13
# PC 8
- "AllOfStart { result: 3, end_pc: 13 }"
# PC 9
- "LoadTrue { dest: 1 }"
# PC 10
- "AllOfNext { check: 1, result: 3, end_pc: 13 }"
# PC 11
- "LoadTrue { dest: 2 }"
# PC 12
- "AllOfNext { check: 2, result: 3, end_pc: 13 }"
# PC 13
- "AllOfEnd { result: 3 }"
# PC 14: AnyOfNext — r3=true → short-circuit! r0=true, skip AnyOfEnd
- "AnyOfNext { check: 3, result: 0, end_pc: 15 }"
# PC 15
- "AnyOfEnd { result: 0 }"
# PC 16
- "Return { value: 0 }"
want_result: true
# =========================================================================
# Nested anyOf inside allOf
# =========================================================================
- note: nested_anyof_in_allof
description: |
allOf [
anyOf [false, true], -- passes (second child true)
anyOf [false, false] -- fails
] → false
literals: []
instructions:
# Outer allOf: AllOfEnd at PC 15
# PC 0
- "AllOfStart { result: 0, end_pc: 15 }"
# --- allOf child 1: anyOf [false, true] ---
# Inner anyOf #1: AnyOfEnd at PC 6
# PC 1
- "AnyOfStart { result: 3, end_pc: 6 }"
# PC 2
- "LoadFalse { dest: 1 }"
# PC 3
- "AnyOfNext { check: 1, result: 3, end_pc: 6 }"
# PC 4
- "LoadTrue { dest: 2 }"
# PC 5: r2=true → short-circuit! r3=true, skip AnyOfEnd
- "AnyOfNext { check: 2, result: 3, end_pc: 6 }"
# PC 6
- "AnyOfEnd { result: 3 }"
# PC 7: AllOfNext — r3=true → no short-circuit, continue
- "AllOfNext { check: 3, result: 0, end_pc: 15 }"
# --- allOf child 2: anyOf [false, false] ---
# Inner anyOf #2: AnyOfEnd at PC 13
# PC 8
- "AnyOfStart { result: 3, end_pc: 13 }"
# PC 9
- "LoadFalse { dest: 1 }"
# PC 10
- "AnyOfNext { check: 1, result: 3, end_pc: 13 }"
# PC 11
- "LoadFalse { dest: 2 }"
# PC 12
- "AnyOfNext { check: 2, result: 3, end_pc: 13 }"
# PC 13
- "AnyOfEnd { result: 3 }"
# PC 14: AllOfNext — r3=false → short-circuit! skip AllOfEnd
- "AllOfNext { check: 3, result: 0, end_pc: 15 }"
# PC 15
- "AllOfEnd { result: 0 }"
# PC 16
- "Return { value: 0 }"
want_result: false
# =========================================================================
# AllOf/AnyOf with policy operators
# =========================================================================
- note: allof_with_policy_equals
description: "allOf [ PolicyEquals(a,a), PolicyEquals(b,B) ] → true"
literals: ["hello", "hello", "World", "WORLD"]
instructions:
# PC 0: AllOfEnd at PC 9
- "AllOfStart { result: 0, end_pc: 9 }"
# Child 1: PolicyEquals("hello", "hello") → r3=true
# PC 1
- "Load { dest: 1, literal_idx: 0 }"
# PC 2
- "Load { dest: 2, literal_idx: 1 }"
# PC 3
- "PolicyEquals { dest: 3, left: 1, right: 2 }"
# PC 4
- "AllOfNext { check: 3, result: 0, end_pc: 9 }"
# Child 2: PolicyEquals("World", "WORLD") → r3=true
# PC 5
- "Load { dest: 1, literal_idx: 2 }"
# PC 6
- "Load { dest: 2, literal_idx: 3 }"
# PC 7
- "PolicyEquals { dest: 3, left: 1, right: 2 }"
# PC 8
- "AllOfNext { check: 3, result: 0, end_pc: 9 }"
# PC 9
- "AllOfEnd { result: 0 }"
# PC 10
- "Return { value: 0 }"
want_result: true
- note: anyof_with_policy_operators
description: "anyOf [ PolicyEquals(a,b), PolicyContains(str,sub) ] → true via second child"
literals: ["hello", "world", "Hello World", "world"]
instructions:
# PC 0
- "AnyOfStart { result: 0, end_pc: 9 }"
# Child 1: PolicyEquals("hello", "world") → false
- "Load { dest: 1, literal_idx: 0 }"
- "Load { dest: 2, literal_idx: 1 }"
- "PolicyEquals { dest: 3, left: 1, right: 2 }"
- "AnyOfNext { check: 3, result: 0, end_pc: 9 }"
# Child 2: PolicyContains("Hello World", "world") → true
- "Load { dest: 1, literal_idx: 2 }"
- "Load { dest: 2, literal_idx: 3 }"
- "PolicyContains { dest: 3, left: 1, right: 2 }"
- "AnyOfNext { check: 3, result: 0, end_pc: 9 }"
# PC 9
- "AnyOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: true
# =========================================================================
# Three children
# =========================================================================
- note: allof_three_children
description: "allOf with three true children"
literals: []
instructions:
- "AllOfStart { result: 0, end_pc: 7 }"
- "LoadTrue { dest: 1 }"
- "AllOfNext { check: 1, result: 0, end_pc: 7 }"
- "LoadTrue { dest: 2 }"
- "AllOfNext { check: 2, result: 0, end_pc: 7 }"
- "LoadTrue { dest: 3 }"
- "AllOfNext { check: 3, result: 0, end_pc: 7 }"
- "AllOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: true
- note: allof_third_false
description: "allOf with third child false"
literals: []
instructions:
- "AllOfStart { result: 0, end_pc: 7 }"
- "LoadTrue { dest: 1 }"
- "AllOfNext { check: 1, result: 0, end_pc: 7 }"
- "LoadTrue { dest: 2 }"
- "AllOfNext { check: 2, result: 0, end_pc: 7 }"
- "LoadFalse { dest: 3 }"
- "AllOfNext { check: 3, result: 0, end_pc: 7 }"
- "AllOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: false
- note: anyof_third_true
description: "anyOf where only third child is true"
literals: []
instructions:
- "AnyOfStart { result: 0, end_pc: 7 }"
- "LoadFalse { dest: 1 }"
- "AnyOfNext { check: 1, result: 0, end_pc: 7 }"
- "LoadFalse { dest: 2 }"
- "AnyOfNext { check: 2, result: 0, end_pc: 7 }"
- "LoadTrue { dest: 3 }"
- "AnyOfNext { check: 3, result: 0, end_pc: 7 }"
- "AnyOfEnd { result: 0 }"
- "Return { value: 0 }"
want_result: true
# =========================================================================
# Additional corner cases
# =========================================================================
- note: anyof_undefined_child
description: "anyOf with undefined child (not true) — continues evaluation"
literals: []
instructions:
# PC 0: AnyOfEnd at PC 5
- "AnyOfStart { result: 0, end_pc: 5 }"
# PC 1: r1 is undefined → AnyOfNext does not short-circuit
- "AnyOfNext { check: 1, result: 0, end_pc: 5 }"
# PC 2: Child 2 = true → short-circuit
- "LoadTrue { dest: 2 }"
# PC 3
- "AnyOfNext { check: 2, result: 0, end_pc: 5 }"
# PC 4: (skipped)
- "LoadFalse { dest: 3 }"
# PC 5
- "AnyOfEnd { result: 0 }"
# PC 6
- "Return { value: 0 }"
want_result: true
- note: anyof_all_undefined
description: "anyOf with all children undefined → false"
literals: []
instructions:
# PC 0: AnyOfEnd at PC 3
- "AnyOfStart { result: 0, end_pc: 3 }"
# PC 1: r1 undefined
- "AnyOfNext { check: 1, result: 0, end_pc: 3 }"
# PC 2: r2 undefined
- "AnyOfNext { check: 2, result: 0, end_pc: 3 }"
# PC 3
- "AnyOfEnd { result: 0 }"
# PC 4
- "Return { value: 0 }"
want_result: false
- note: allof_all_undefined
description: "allOf with all children undefined → false (short-circuits on first)"
literals: []
instructions:
# PC 0: AllOfEnd at PC 3
- "AllOfStart { result: 0, end_pc: 3 }"
# PC 1: r1 undefined → short-circuit
- "AllOfNext { check: 1, result: 0, end_pc: 3 }"
# PC 2: r2 undefined (skipped)
- "AllOfNext { check: 2, result: 0, end_pc: 3 }"
# PC 3
- "AllOfEnd { result: 0 }"
# PC 4
- "Return { value: 0 }"
want_result: false
- note: allof_middle_false
description: "allOf with 3 children, only middle fails → false"
literals: []
instructions:
# PC 0: AllOfEnd at PC 7
- "AllOfStart { result: 0, end_pc: 7 }"
# PC 1
- "LoadTrue { dest: 1 }"
# PC 2
- "AllOfNext { check: 1, result: 0, end_pc: 7 }"
# PC 3: middle child = false
- "LoadFalse { dest: 2 }"
# PC 4: short-circuits
- "AllOfNext { check: 2, result: 0, end_pc: 7 }"
# PC 5 (skipped)
- "LoadTrue { dest: 3 }"
# PC 6 (skipped)
- "AllOfNext { check: 3, result: 0, end_pc: 7 }"
# PC 7 (skipped)
- "AllOfEnd { result: 0 }"
# PC 8
- "Return { value: 0 }"
want_result: false
- note: anyof_middle_true
description: "anyOf with 3 children, only middle is true → true"
literals: []
instructions:
# PC 0: AnyOfEnd at PC 7
- "AnyOfStart { result: 0, end_pc: 7 }"
# PC 1
- "LoadFalse { dest: 1 }"
# PC 2
- "AnyOfNext { check: 1, result: 0, end_pc: 7 }"
# PC 3: middle child = true → short-circuit
- "LoadTrue { dest: 2 }"
# PC 4
- "AnyOfNext { check: 2, result: 0, end_pc: 7 }"
# PC 5 (skipped)
- "LoadFalse { dest: 3 }"
# PC 6 (skipped)
- "AnyOfNext { check: 3, result: 0, end_pc: 7 }"
# PC 7 (skipped)
- "AnyOfEnd { result: 0 }"
# PC 8
- "Return { value: 0 }"
want_result: true
- note: nested_allof_both_inner_pass
description: |
anyOf [
allOf [true, true], -- passes → anyOf short-circuits
allOf [true, true] -- skipped
] → true (verify short-circuit on first passing inner)
literals: []
instructions:
# Outer anyOf: AnyOfEnd at PC 15
# PC 0
- "AnyOfStart { result: 0, end_pc: 15 }"
# Inner allOf #1: AllOfEnd at PC 6
# PC 1
- "AllOfStart { result: 3, end_pc: 6 }"
# PC 2
- "LoadTrue { dest: 1 }"
# PC 3
- "AllOfNext { check: 1, result: 3, end_pc: 6 }"
# PC 4
- "LoadTrue { dest: 2 }"
# PC 5
- "AllOfNext { check: 2, result: 3, end_pc: 6 }"
# PC 6
- "AllOfEnd { result: 3 }"
# PC 7: r3=true → short-circuit! → skip to PC 16
- "AnyOfNext { check: 3, result: 0, end_pc: 15 }"
# Inner allOf #2 (ALL SKIPPED)
# PC 8
- "AllOfStart { result: 3, end_pc: 13 }"
# PC 9
- "LoadTrue { dest: 1 }"
# PC 10
- "AllOfNext { check: 1, result: 3, end_pc: 13 }"
# PC 11
- "LoadTrue { dest: 2 }"
# PC 12
- "AllOfNext { check: 2, result: 3, end_pc: 13 }"
# PC 13
- "AllOfEnd { result: 3 }"
# PC 14
- "AnyOfNext { check: 3, result: 0, end_pc: 15 }"
# PC 15
- "AnyOfEnd { result: 0 }"
# PC 16
- "Return { value: 0 }"
want_result: true
- note: register_isolation
description: |
allOf(r0) [ anyOf(r3) [true] ] → true.
Inner anyOf uses r3, outer allOf uses r0.
Verifies different result registers don't interfere.
literals: []
instructions:
# Outer allOf: r0, AllOfEnd at PC 6
# PC 0
- "AllOfStart { result: 0, end_pc: 6 }"
# Inner anyOf: r3, AnyOfEnd at PC 4
# PC 1
- "AnyOfStart { result: 3, end_pc: 4 }"
# PC 2
- "LoadTrue { dest: 1 }"
# PC 3: r1=true → r3=true, pc=4, +1→5 (skip AnyOfEnd)
- "AnyOfNext { check: 1, result: 3, end_pc: 4 }"
# PC 4
- "AnyOfEnd { result: 3 }"
# PC 5: AllOfNext — r3=true → continue
- "AllOfNext { check: 3, result: 0, end_pc: 6 }"
# PC 6
- "AllOfEnd { result: 0 }"
# PC 7
- "Return { value: 0 }"
want_result: true
- note: allof_with_policy_eq_first_fails
description: "allOf [ PolicyEquals(a,b), PolicyEquals(c,c) ] → false (first child fails)"
literals: ["hello", "world", "same", "SAME"]
instructions:
# PC 0: AllOfEnd at PC 9
- "AllOfStart { result: 0, end_pc: 9 }"
# Child 1: PolicyEquals("hello", "world") → r3=false
# PC 1
- "Load { dest: 1, literal_idx: 0 }"
# PC 2
- "Load { dest: 2, literal_idx: 1 }"
# PC 3
- "PolicyEquals { dest: 3, left: 1, right: 2 }"
# PC 4: short-circuits → skip AllOfEnd
- "AllOfNext { check: 3, result: 0, end_pc: 9 }"
# Child 2 (skipped)
# PC 5
- "Load { dest: 1, literal_idx: 2 }"
# PC 6
- "Load { dest: 2, literal_idx: 3 }"
# PC 7
- "PolicyEquals { dest: 3, left: 1, right: 2 }"
# PC 8
- "AllOfNext { check: 3, result: 0, end_pc: 9 }"
# PC 9
- "AllOfEnd { result: 0 }"
# PC 10
- "Return { value: 0 }"
want_result: false
- note: nested_3_deep
description: |
allOf [
anyOf [
allOf [true, false], -- inner allOf fails
allOf [true, true] -- inner allOf passes → anyOf passes
]
] → true
literals: []
instructions:
# Outer allOf: AllOfEnd at PC 18
# PC 0
- "AllOfStart { result: 0, end_pc: 18 }"
# Middle anyOf: AnyOfEnd at PC 16
# PC 1
- "AnyOfStart { result: 4, end_pc: 16 }"
# Inner allOf #1: [true, false] — AllOfEnd at PC 7
# PC 2
- "AllOfStart { result: 3, end_pc: 7 }"
# PC 3
- "LoadTrue { dest: 1 }"
# PC 4
- "AllOfNext { check: 1, result: 3, end_pc: 7 }"
# PC 5
- "LoadFalse { dest: 2 }"
# PC 6: r2=false → short-circuit → pc=7, +1→8
- "AllOfNext { check: 2, result: 3, end_pc: 7 }"
# PC 7: AllOfEnd (skipped when short-circuit, r3 stays false)
- "AllOfEnd { result: 3 }"
# PC 8: AnyOfNext — r3=false → no short-circuit, continue
- "AnyOfNext { check: 3, result: 4, end_pc: 16 }"
# Inner allOf #2: [true, true] — AllOfEnd at PC 14
# PC 9
- "AllOfStart { result: 3, end_pc: 14 }"
# PC 10
- "LoadTrue { dest: 1 }"
# PC 11
- "AllOfNext { check: 1, result: 3, end_pc: 14 }"
# PC 12
- "LoadTrue { dest: 2 }"
# PC 13
- "AllOfNext { check: 2, result: 3, end_pc: 14 }"
# PC 14: AllOfEnd — r3=true
- "AllOfEnd { result: 3 }"
# PC 15: AnyOfNext — r3=true → r4=true, pc=16, +1→17
- "AnyOfNext { check: 3, result: 4, end_pc: 16 }"
# PC 16: AnyOfEnd (skipped on short-circuit)
- "AnyOfEnd { result: 4 }"
# PC 17: AllOfNext — r4=true → continue
- "AllOfNext { check: 4, result: 0, end_pc: 18 }"
# PC 18: AllOfEnd — r0=true
- "AllOfEnd { result: 0 }"
# PC 19
- "Return { value: 0 }"
want_result: true

File diff suppressed because it is too large Load Diff