From 4d35744c4f92d68d59b87bed5aa657fc661e0c3c Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:04:24 -0500 Subject: [PATCH] feat(rvm): implement Azure Policy condition evaluation (#661) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/builtins/azure_policy/helpers.rs | 247 +++ src/rvm/instructions/display.rs | 47 +- src/rvm/instructions/mod.rs | 63 +- src/rvm/instructions/types.rs | 112 ++ src/rvm/program/listing.rs | 22 + src/rvm/tests/instruction_parser.rs | 117 +- src/rvm/tests/vm.rs | 6 + src/rvm/vm/dispatch.rs | 291 +++ .../azure_policy/policy_allof_anyof.yaml | 680 +++++++ .../suites/azure_policy/policy_operators.yaml | 1583 +++++++++++++++++ 10 files changed, 3165 insertions(+), 3 deletions(-) create mode 100644 tests/rvm/vm/suites/azure_policy/policy_allof_anyof.yaml create mode 100644 tests/rvm/vm/suites/azure_policy/policy_operators.yaml diff --git a/src/builtins/azure_policy/helpers.rs b/src/builtins/azure_policy/helpers.rs index bd8a66b..f9eadad 100644 --- a/src/builtins/azure_policy/helpers.rs +++ b/src/builtins/azure_policy/helpers.rs @@ -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 { + 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 { + 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 { + 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 { + 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. pub fn try_coerce_to_number(s: &str) -> Option { use core::str::FromStr as _; @@ -61,6 +211,103 @@ pub fn try_coerce_to_number(s: &str) -> Option { }) } +// ── 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 = 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 ─────────────────────────────────────────────────── pub fn resolve_path(root: &Value, path: &str) -> Value { diff --git a/src/rvm/instructions/display.rs b/src/rvm/instructions/display.rs index 4fd526c..1b8a00f 100644 --- a/src/rvm/instructions/display.rs +++ b/src/rvm/instructions/display.rs @@ -5,7 +5,7 @@ use alloc::format; use alloc::string::String; use alloc::vec::Vec; -use super::types::GuardMode; +use super::types::{GuardMode, LogicalBlockMode, PolicyOp}; use super::{Instruction, InstructionData, LiteralOrRegister}; impl Instruction { @@ -291,6 +291,51 @@ impl core::fmt::Display for Instruction { |k| format!("COMPREHENSION_YIELD R({}) R({})", k, value_reg), ), 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) } diff --git a/src/rvm/instructions/mod.rs b/src/rvm/instructions/mod.rs index b98b583..82fe0f7 100644 --- a/src/rvm/instructions/mod.rs +++ b/src/rvm/instructions/mod.rs @@ -10,7 +10,9 @@ pub use params::{ FunctionCallParams, InstructionData, LoopStartParams, ObjectCreateParams, SetCreateParams, VirtualDataDocumentLookupParams, }; -pub use types::{ComprehensionMode, GuardMode, LiteralOrRegister, LoopMode}; +pub use types::{ + ComprehensionMode, GuardMode, LiteralOrRegister, LogicalBlockMode, LoopMode, PolicyOp, +}; use serde::{Deserialize, Serialize}; @@ -355,6 +357,65 @@ pub enum Instruction { /// End a comprehension block 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 { diff --git a/src/rvm/instructions/types.rs b/src/rvm/instructions/types.rs index ccff662..082cf0e 100644 --- a/src/rvm/instructions/types.rs +++ b/src/rvm/instructions/types.rs @@ -46,6 +46,110 @@ pub enum ComprehensionMode { 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. #[repr(u8)] #[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. 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, +} diff --git a/src/rvm/program/listing.rs b/src/rvm/program/listing.rs index 9de5bac..9b825e8 100644 --- a/src/rvm/program/listing.rs +++ b/src/rvm/program/listing.rs @@ -911,6 +911,15 @@ fn format_instruction_readable( let base = format!("{}}} CompEnd", indent); 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::ComprehensionYield { .. } => "COMP_YIELD", 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", + }, } } diff --git a/src/rvm/tests/instruction_parser.rs b/src/rvm/tests/instruction_parser.rs index a38d798..5d32375 100644 --- a/src/rvm/tests/instruction_parser.rs +++ b/src/rvm/tests/instruction_parser.rs @@ -9,7 +9,7 @@ clippy::pattern_type_mismatch )] // 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::vec::Vec; use anyhow::{anyhow, bail, Result}; @@ -83,6 +83,41 @@ pub fn parse_instruction(text: &str) -> Result { "ComprehensionYield" => parse_comprehension_add(params_text), "ReturnUndefinedIfNotTrue" => parse_return_undefined_if_not_true(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), } } else { @@ -707,3 +742,83 @@ fn parse_coalesce_undefined_to_null(params_text: &str) -> Result { 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 { + let params = parse_params(params_text)?; + let dest: u8 = get_param_u16(¶ms, "dest")?.try_into().unwrap(); + let left: u8 = get_param_u16(¶ms, "left")?.try_into().unwrap(); + let right: u8 = get_param_u16(¶ms, "right")?.try_into().unwrap(); + Ok(Instruction::PolicyCondition { + dest, + left, + right, + op, + }) +} + +fn parse_value_condition_guard(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest: u8 = get_param_u16(¶ms, "dest")?.try_into().unwrap(); + let value: u8 = get_param_u16(¶ms, "value")?.try_into().unwrap(); + let condition: u8 = get_param_u16(¶ms, "condition")?.try_into().unwrap(); + Ok(Instruction::PolicyCondition { + dest, + left: value, + right: condition, + op: PolicyOp::ValueConditionGuard, + }) +} + +fn parse_policy_not(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let dest: u8 = get_param_u16(¶ms, "dest")?.try_into().unwrap(); + let operand: u8 = get_param_u16(¶ms, "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 { + let params = parse_params(params_text)?; + let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap(); + let end_pc = get_param_u16(¶ms, "end_pc")?; + Ok(Instruction::LogicalBlockStart { + mode, + result, + end_pc, + }) +} + +fn parse_allof_next(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let check: u8 = get_param_u16(¶ms, "check")?.try_into().unwrap(); + let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap(); + let end_pc = get_param_u16(¶ms, "end_pc")?; + Ok(Instruction::AllOfNext { + check, + result, + end_pc, + }) +} + +fn parse_logical_block_end(params_text: &str, mode: LogicalBlockMode) -> Result { + let params = parse_params(params_text)?; + let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap(); + Ok(Instruction::LogicalBlockEnd { mode, result }) +} + +fn parse_anyof_next(params_text: &str) -> Result { + let params = parse_params(params_text)?; + let check: u8 = get_param_u16(¶ms, "check")?.try_into().unwrap(); + let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap(); + let end_pc = get_param_u16(¶ms, "end_pc")?; + Ok(Instruction::AnyOfNext { + check, + result, + end_pc, + }) +} diff --git a/src/rvm/tests/vm.rs b/src/rvm/tests/vm.rs index 7d1250d..3bd1089 100644 --- a/src/rvm/tests/vm.rs +++ b/src/rvm/tests/vm.rs @@ -1059,4 +1059,10 @@ mod tests { fn run_loop_test_file(file: &str) { 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() + } } diff --git a/src/rvm/vm/dispatch.rs b/src/rvm/vm/dispatch.rs index bcc6b55..d0cb044 100644 --- a/src/rvm/vm/dispatch.rs +++ b/src/rvm/vm/dispatch.rs @@ -807,6 +807,297 @@ impl RegoVM { let result = self.get_register(0)?.clone(); 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 { + 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 { + 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 { + 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), } } diff --git a/tests/rvm/vm/suites/azure_policy/policy_allof_anyof.yaml b/tests/rvm/vm/suites/azure_policy/policy_allof_anyof.yaml new file mode 100644 index 0000000..84c47a3 --- /dev/null +++ b/tests/rvm/vm/suites/azure_policy/policy_allof_anyof.yaml @@ -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 diff --git a/tests/rvm/vm/suites/azure_policy/policy_operators.yaml b/tests/rvm/vm/suites/azure_policy/policy_operators.yaml new file mode 100644 index 0000000..2e49d1a --- /dev/null +++ b/tests/rvm/vm/suites/azure_policy/policy_operators.yaml @@ -0,0 +1,1583 @@ +# Azure Policy condition operator instruction tests +# +# Tests for all 20 condition operators + PolicyNot, including +# ValueConditionGuard short-circuit behavior. +# Each operator implements Azure Policy semantics (case-insensitive, +# type coercion, undefined-as-missing-field behavior). + +cases: + # ========================================================================= + # ValueConditionGuard — short-circuit on undefined value + # ========================================================================= + + - note: value_condition_guard_undefined_value + description: "undefined value register → dest=false (short-circuit)" + literals: [] + instructions: + - "LoadTrue { dest: 2 }" + - "ValueConditionGuard { dest: 0, value: 1, condition: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: value_condition_guard_defined_value + description: "defined value register → dest copies condition register" + literals: ["hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "LoadTrue { dest: 2 }" + - "ValueConditionGuard { dest: 0, value: 1, condition: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: value_condition_guard_defined_value_false_condition + description: "defined value register, false condition → dest=false" + literals: ["hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "LoadFalse { dest: 2 }" + - "ValueConditionGuard { dest: 0, value: 1, condition: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # PolicyEquals — case-insensitive equality with type coercion + # ========================================================================= + + - note: policy_equals_same_strings + literals: ["hello", "hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_equals_case_insensitive + literals: ["Hello", "hELLO"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_equals_different_strings + literals: ["hello", "world"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_equals_numbers + literals: [42, 42] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_equals_string_number_coercion + description: String "42" should equal number 42 via type coercion + literals: ["42", 42] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_equals_booleans + literals: [true, true] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_equals_undefined_left + description: undefined left operand returns false + literals: ["hello"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_equals_null_values + literals: [] + instructions: + - "LoadNull { dest: 1 }" + - "LoadNull { dest: 2 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_equals_undefined_vs_null + description: "undefined LHS + null RHS → true (missing field sentinel)" + literals: [] + instructions: + - "LoadNull { dest: 2 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # PolicyNotEquals — undefined field → true, else negated CI equality + # ========================================================================= + + - note: policy_not_equals_same_strings + literals: ["hello", "hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_equals_different_strings + literals: ["hello", "world"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_equals_undefined_left + description: "undefined field → true (field doesn't exist, so not equal)" + literals: ["hello"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyNotEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_equals_undefined_vs_null + description: "undefined LHS + null RHS → false (missing field sentinel)" + literals: [] + instructions: + - "LoadNull { dest: 2 }" + - "PolicyNotEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_equals_case_insensitive + literals: ["Hello", "hELLO"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # PolicyGreater / PolicyGreaterOrEquals / PolicyLess / PolicyLessOrEquals + # ========================================================================= + + - note: policy_greater_numbers + literals: [10, 5] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyGreater { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_greater_strict_equal_numbers + description: "strict greater with equal numbers → false" + literals: [5, 5] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyGreater { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_greater_strings_ci + description: "CI string comparison: 'banana' > 'Apple'" + literals: ["banana", "Apple"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyGreater { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_greater_undefined + description: "undefined operand returns false" + literals: [10] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyGreater { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_greater_or_equals_true + literals: [5, 5] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyGreaterOrEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_greater_or_equals_less + literals: [3, 5] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyGreaterOrEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_less_true + literals: [3, 10] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLess { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_less_false + literals: [10, 3] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLess { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_less_or_equals_equal + literals: [5, 5] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLessOrEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_less_or_equals_greater + literals: [10, 5] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLessOrEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_greater_string_number_coercion + description: "String '10' > number 5 via coercion" + literals: ["10", 5] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyGreater { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # PolicyIn / PolicyNotIn — CI array/set membership + # ========================================================================= + + - note: policy_in_array_match + literals: ["hello", ["Hello", "World"]] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_in_array_no_match + literals: ["foo", ["Hello", "World"]] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_in_undefined_left + literals: [["Hello"]] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_in_not_array + description: "scalar right operand is treated as singleton; non-matching value → false" + literals: ["hello", "world"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_in_match + description: "value IS in array → false" + literals: ["hello", ["Hello", "World"]] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_in_no_match + description: "value NOT in array → true" + literals: ["foo", ["Hello", "World"]] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_in_undefined_left + description: "undefined field → true" + literals: [["Hello"]] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyNotIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # PolicyContains / PolicyNotContains + # ========================================================================= + + - note: policy_contains_substring + literals: ["Hello World", "world"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_contains_substring_no_match + literals: ["Hello World", "foo"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_contains_array_element + literals: [["Hello", "World"], "hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_contains_undefined + literals: ["hello"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_contains_substring + literals: ["Hello World", "foo"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_contains_undefined_left + description: "undefined field → true" + literals: ["hello"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyNotContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # PolicyContainsKey / PolicyNotContainsKey + # ========================================================================= + + - note: policy_contains_key_match + literals: + - {"Name": "test", "Value": 42} + - "name" + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContainsKey { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_contains_key_no_match + literals: + - {"Name": "test"} + - "missing" + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContainsKey { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_contains_key_undefined + literals: ["name"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyContainsKey { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_contains_key_missing + literals: + - {"Name": "test"} + - "missing" + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotContainsKey { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_contains_key_undefined + description: "undefined field → true" + literals: ["name"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyNotContainsKey { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # PolicyLike / PolicyNotLike — wildcard glob (*, ?) + # ========================================================================= + + - note: policy_like_match + literals: ["hello world", "hello*"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_like_question + literals: ["cat", "c?t"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_like_no_match + literals: ["hello", "world*"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_like_case_insensitive + literals: ["HELLO", "hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_like_no_match + literals: ["hello", "world*"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_like_undefined + description: "undefined field → true" + literals: ["hello*"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyNotLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # PolicyMatch / PolicyNotMatch — case-sensitive (?, #) + # ========================================================================= + + - note: policy_match_exact + literals: ["a1b2", "?#?#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_match_no_match + literals: ["1234", "?#?#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_match_dot_wildcard + description: "dot wildcard matches any single character" + literals: ["a1", ".#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_match_insensitively_dot_wildcard + description: "dot wildcard with case-insensitive matching" + literals: ["A1", ".#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatchInsensitively { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_match_length_mismatch + literals: ["ab", "?#?#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_match_true + literals: ["1234", "?#?#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_match_undefined + description: "undefined field → true" + literals: ["?#"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyNotMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # PolicyMatchInsensitively / PolicyNotMatchInsensitively + # ========================================================================= + + - note: policy_match_insensitively_true + description: "Case-insensitive ?, # match" + literals: ["A1", "?#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatchInsensitively { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_match_insensitively_case + description: "Case-insensitive match: 'Ab' matches '??' (lowercased)" + literals: ["Ab", "??"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatchInsensitively { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_match_insensitively_undefined + description: "undefined field → true" + literals: ["?#"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyNotMatchInsensitively { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_match_insensitively_match + description: "matching pattern → false" + literals: ["a1", "?#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotMatchInsensitively { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # PolicyExists + # ========================================================================= + + - note: policy_exists_true_defined + description: "Defined non-null value, expected=true → true" + literals: ["hello", "true"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyExists { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_exists_false_undefined + description: "Undefined value, expected=true → false" + literals: ["true"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyExists { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_exists_false_null + description: "Null value, expected=true → false (null treated as not-exists)" + literals: ["true"] + instructions: + - "LoadNull { dest: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyExists { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_exists_not_expected_undefined + description: "Undefined value, expected=false → true (field doesn't exist, as expected)" + literals: ["false"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyExists { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_exists_not_expected_defined + description: "Defined value, expected=false → false (field exists, but we expected it not to)" + literals: ["hello", "false"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyExists { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_exists_bool_true + description: "expected as bool true" + literals: ["hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "LoadTrue { dest: 2 }" + - "PolicyExists { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # PolicyNot — !is_true(operand) + # ========================================================================= + + - note: policy_not_true + literals: [] + instructions: + - "LoadTrue { dest: 1 }" + - "PolicyNot { dest: 0, operand: 1 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_false + literals: [] + instructions: + - "LoadFalse { dest: 1 }" + - "PolicyNot { dest: 0, operand: 1 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_undefined + description: "undefined is not true → PolicyNot returns true" + literals: [] + instructions: + - "PolicyNot { dest: 0, operand: 1 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_null + description: "null is not true → PolicyNot returns true" + literals: [] + instructions: + - "LoadNull { dest: 1 }" + - "PolicyNot { dest: 0, operand: 1 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_number + description: "number is not true → PolicyNot returns true" + literals: [42] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyNot { dest: 0, operand: 1 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_string_true + description: "string 'true' is not bool true → PolicyNot returns true" + literals: ["true"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyNot { dest: 0, operand: 1 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # Additional corner cases — PolicyEquals + # ========================================================================= + + - note: policy_equals_both_undefined + description: "both operands undefined → case_insensitive_equals returns false (any undefined → false)" + literals: [] + instructions: + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_equals_undefined_right + description: "defined left, undefined right" + literals: ["hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_equals_bool_vs_string + description: "bool true vs string 'true' — equal (String↔Bool coercion)" + literals: ["true"] + instructions: + - "LoadTrue { dest: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_equals_empty_strings + literals: ["", ""] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_equals_number_string_reversed + description: "number 42 vs string '42' — reversed order from policy_equals_string_number_coercion" + literals: [42, "42"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_equals_different_numbers + literals: [42, 99] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_equals_null_vs_undefined + description: "null vs undefined → false (any undefined → false)" + literals: [] + instructions: + - "LoadNull { dest: 1 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_equals_false_vs_false + literals: [] + instructions: + - "LoadFalse { dest: 1 }" + - "LoadFalse { dest: 2 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_equals_true_vs_false + literals: [] + instructions: + - "LoadTrue { dest: 1 }" + - "LoadFalse { dest: 2 }" + - "PolicyEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # Additional corner cases — PolicyNotEquals + # ========================================================================= + + - note: policy_not_equals_both_undefined + description: "both undefined → left is undefined so returns !matches!(r, Value::Null); right is also undefined (not Null) → true" + literals: [] + instructions: + - "PolicyNotEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_equals_undefined_right + description: "defined left, undefined right" + literals: ["hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyNotEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_equals_null_null + description: "null vs null → equal → false" + literals: [] + instructions: + - "LoadNull { dest: 1 }" + - "LoadNull { dest: 2 }" + - "PolicyNotEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # Additional corner cases — Comparison operators + # ========================================================================= + + - note: policy_greater_negative_numbers + literals: [-5, -10] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyGreater { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_greater_undefined_right + description: "undefined right operand → compare_values returns None → false" + literals: [10] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyGreater { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_greater_both_undefined + literals: [] + instructions: + - "PolicyGreater { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_less_strings_ci + description: "CI string comparison: 'Apple' < 'banana'" + literals: ["Apple", "banana"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLess { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_less_equal_strings + description: "same string CI → equal → not less" + literals: ["Hello", "hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLess { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_less_or_equals_strings_equal_ci + description: "same string CI → ≤ is true" + literals: ["Hello", "hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLessOrEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_greater_or_equals_strings_equal_ci + description: "same string CI → ≥ is true" + literals: ["Hello", "hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyGreaterOrEquals { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_less_mixed_types + description: "string vs bool → compare_values returns None → false" + literals: ["hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "LoadTrue { dest: 2 }" + - "PolicyLess { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # Additional corner cases — PolicyIn / PolicyNotIn + # ========================================================================= + + - note: policy_in_empty_array + description: "value in empty array → false" + literals: ["hello", []] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_in_number_coercion + description: "number 42 in array ['42'] — CI equality coerces" + literals: [42, ["42"]] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_in_undefined_right + description: "defined left, undefined right → false" + literals: ["hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_in_both_undefined + literals: [] + instructions: + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_in_undefined_right + description: "defined left, undefined right → true (not in missing collection)" + literals: ["hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyNotIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_in_empty_array + description: "value not in empty array → true" + literals: ["hello", []] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_in_not_array + description: "right operand is not array → treated as single-element collection; non-matching value → true" + literals: ["hello", "world"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # Additional corner cases — PolicyContains / PolicyNotContains + # ========================================================================= + + - note: policy_contains_empty_substring + description: "empty substring is always contained" + literals: ["hello", ""] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_contains_empty_haystack + description: "non-empty needle in empty haystack" + literals: ["", "hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_contains_array_no_match + description: "array does not contain the element" + literals: [["Hello", "World"], "foo"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_contains_undefined_right + description: "defined left, undefined right → false" + literals: ["hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_contains_both_undefined + literals: [] + instructions: + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_contains_number_not_string + description: "left is number, not string/array → false" + literals: [42, 4] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_contains_undefined_right + description: "defined left, undefined right → false" + literals: ["hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyNotContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_contains_match + description: "substring found → false" + literals: ["Hello World", "world"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # Additional corner cases — PolicyContainsKey / PolicyNotContainsKey + # ========================================================================= + + - note: policy_contains_key_multiple_keys + description: "object with multiple keys, one matches CI" + literals: + - {"Alpha": 1, "Beta": 2, "Gamma": 3} + - "beta" + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContainsKey { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_contains_key_not_object + description: "left is not an object → false" + literals: ["not an object", "key"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContainsKey { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_contains_key_undefined_right + description: "defined object, undefined right → false" + literals: + - {"Name": "test"} + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyContainsKey { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_contains_key_present + description: "key IS present → false" + literals: + - {"Name": "test"} + - "name" + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotContainsKey { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_contains_key_undefined_right + description: "defined object, undefined right → false" + literals: + - {"Name": "test"} + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyNotContainsKey { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # Additional corner cases — PolicyLike / PolicyNotLike + # ========================================================================= + + - note: policy_like_multiple_wildcards + description: "pattern with multiple * wildcards" + literals: ["hello beautiful world", "*beautiful*"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_like_star_only + description: "pattern '*' matches everything" + literals: ["anything at all", "*"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_like_empty_vs_empty + description: "empty input, empty pattern" + literals: ["", ""] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_like_undefined_left + description: "undefined left → as_string_ci returns None → false" + literals: ["hello*"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_like_exact_match + description: "no wildcard, must match exactly (CI)" + literals: ["Hello", "hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_like_question_at_end + description: "? at end requires exactly one char" + literals: ["ab", "a?"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_like_question_length_mismatch + description: "? requires exactly one char — too long" + literals: ["abc", "a?"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_like_match + description: "pattern matches → false" + literals: ["hello world", "hello*"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # Additional corner cases — PolicyLike with multi-byte UTF-8 + # ========================================================================= + + - note: policy_like_utf8_question + description: "? matches a single multi-byte char — 'café' matches 'caf?'" + literals: ["café", "caf?"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_like_utf8_star + description: "* matches multi-byte prefix — 'über' matches '*ber'" + literals: ["über", "*ber"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyLike { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # Additional corner cases — PolicyMatch case sensitivity + # ========================================================================= + + - note: policy_match_case_insensitive_letter + description: "? matches any letter regardless of case — 'A1' matches '?#'" + literals: ["A1", "?#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_match_literal_chars + description: "literal chars in pattern must match exactly" + literals: ["abc", "abc"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_match_undefined_left + description: "undefined → match_pattern returns false" + literals: ["?#"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_match_hash_only + description: "# matches digit only" + literals: ["5", "#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_match_hash_letter_fail + description: "# does NOT match a letter" + literals: ["a", "#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_match_question_digit + description: "? matches only letters, NOT digits — so '5' vs '?' → false" + literals: ["5", "?"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_match_false + description: "pattern matches → false" + literals: ["a1", "?#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotMatch { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # Additional corner cases — PolicyMatchInsensitively + # ========================================================================= + + - note: policy_match_insensitively_hash_digit + description: "# matches digit, case-insensitive mode" + literals: ["3", "#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatchInsensitively { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_match_insensitively_no_match + description: "pattern fails even case-insensitively" + literals: ["abc", "?#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyMatchInsensitively { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_match_insensitively_undefined + description: "undefined left → false" + literals: ["?#"] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyMatchInsensitively { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_match_insensitively_no_match + description: "pattern doesn't match → true" + literals: ["abc", "?#"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyNotMatchInsensitively { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + # ========================================================================= + # Additional corner cases — PolicyExists + # ========================================================================= + + - note: policy_exists_undefined_right + description: "undefined right → as_boolish returns None → expected=false → is_defined!=false" + literals: ["hello"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "PolicyExists { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_exists_both_undefined + description: "both undefined: is_defined=false, expected=false → true" + literals: [] + instructions: + - "PolicyExists { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_exists_string_false_defined + description: "defined value, expected='false' → is_defined=true != expected=false → false" + literals: ["hello", "false"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyExists { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_exists_bool_false_undefined + description: "undefined value, expected=false → is_defined=false == expected=false → true" + literals: [] + instructions: + - "LoadFalse { dest: 2 }" + - "PolicyExists { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_exists_non_bool_right + description: "right is number → as_boolish returns None → expected=false" + literals: [42, "hello"] + instructions: + - "Load { dest: 1, literal_idx: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyExists { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # Edge cases — PolicyIn/PolicyNotIn with scalar RHS, null LHS, null sentinel + # ========================================================================= + + - note: policy_in_scalar_rhs_match + description: "scalar RHS is treated as single-element collection — matching value → true" + literals: ["hello", "HELLO"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_in_null_lhs + description: "explicit null LHS always returns false (null is a sentinel, not a matchable value)" + literals: [["hello", null]] + instructions: + - "LoadNull { dest: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_in_null_lhs + description: "explicit null LHS — notIn always returns true" + literals: [["hello", null]] + instructions: + - "LoadNull { dest: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyNotIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_in_null_lhs_null_only_array + description: "null LHS against [null] — null-as-defined doesn't match null-as-sentinel" + literals: [[null]] + instructions: + - "LoadNull { dest: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_not_in_null_lhs_null_only_array + description: "null LHS against [null] — notIn always true for null-as-defined" + literals: [[null]] + instructions: + - "LoadNull { dest: 1 }" + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyNotIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_in_rhs_array_with_null_sentinel + description: "non-null LHS, RHS array has null sentinel — null is skipped, no match" + literals: ["hello", [null, "world"]] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_in_undefined_lhs_null_sentinel + description: "undefined LHS, RHS array has null sentinel → true (null means 'match missing')" + literals: [[null, "world"]] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_not_in_undefined_lhs_null_sentinel + description: "undefined LHS, RHS array has null sentinel → false (null matches missing)" + literals: [[null, "world"]] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyNotIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_in_undefined_lhs_no_null_sentinel + description: "undefined LHS, RHS array has no null → false (no sentinel match)" + literals: [["hello", "world"]] + instructions: + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyIn { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + # ========================================================================= + # Edge cases — PolicyContains with object LHS, set LHS, scalar coercion + # ========================================================================= + + - note: policy_contains_object_key_match + description: "object LHS — contains checks keys only, CI match" + literals: [{"Alpha": 1, "beta": 2}, "ALPHA"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_contains_object_key_no_match + description: "object LHS — key not present" + literals: [{"Alpha": 1, "beta": 2}, "gamma"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_contains_object_value_not_checked + description: "object LHS — value exists but is not a key, so no match" + literals: [{"Alpha": "target"}, "target"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false + + - note: policy_contains_set_element_match + description: "set LHS — element match, case insensitive" + literals: ["HELLO"] + instruction_params: + set_create_params: + - dest: 1 + elements: [3] + instructions: + - "Load { dest: 3, literal_idx: 0 }" + - "SetCreate { params_index: 0 }" + - "Load { dest: 2, literal_idx: 0 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_contains_number_lhs_string_rhs + description: "number LHS coerced to string when RHS is string — 42 contains '4'" + literals: [42, "4"] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: true + + - note: policy_contains_number_lhs_number_rhs + description: "number LHS, number RHS (not string) — no coercion path → false" + literals: [42, 4] + instructions: + - "Load { dest: 1, literal_idx: 0 }" + - "Load { dest: 2, literal_idx: 1 }" + - "PolicyContains { dest: 0, left: 1, right: 2 }" + - "Return { value: 0 }" + want_result: false