feat(rvm): new instructions and loop semantics for Azure Policy support (#659)

The Rego VM was designed around Rego's semantics, but Azure Policy needs
a few things Rego doesn't: host-supplied context alongside input/data,
undefined-to-null coercion for missing fields, skip-undefined collection
behavior for wildcard aliases, and non-vacuous iteration over non-array
values.

This commit adds five new instructions to bridge those gaps:

  LoadContext / LoadMetadata — give programs access to host-supplied
  evaluation context and cached program metadata at runtime.

  ArrayPushDefined — like ArrayPush but silently drops undefined values,
  so wildcard alias collection (field[*].property) excludes absent
  nested properties instead of leaking undefined entries into the array.

  ReturnUndefinedIfNotTrue — early return with Undefined when a guard
  condition isn't satisfied, without tripping a VM assertion failure.
  This models "condition doesn't match" cleanly.

  CoalesceUndefinedToNull — turns Undefined into Null in-place so that
  downstream builtins see null rather than short-circuiting on undefined.

The loop engine also gains an Azure Policy mode: when the source language
is "azure_policy", an Every loop over a non-array value (scalars, null,
objects) iterates once over a virtual Null element instead of being
vacuously true.  This matches how field[*] behaves on non-array fields
in Azure Policy — the condition body runs once against Null, which
typically evaluates to false.

On the plumbing side: the VM gets a context field with set_context(),
metadata is cached as a Value on program load, and map_limit_error is
inlined into memory_check since it had only one call site.

Four new YAML test suites (~880 lines) cover the new instructions and
context/metadata loading, along with instruction parser, display, and
assembly listing support for everything added here.
This commit is contained in:
Anand Krishnamoorthi
2026-04-06 15:40:41 -05:00
committed by GitHub
parent 8f740e2f6f
commit e5ac9a2734
15 changed files with 1215 additions and 25 deletions

View File

@@ -143,6 +143,8 @@ impl core::fmt::Display for Instruction {
Instruction::LoadBool { dest, value } => format!("LOAD_BOOL R({}) {}", dest, value),
Instruction::LoadData { dest } => format!("LOAD_DATA R({})", dest),
Instruction::LoadInput { dest } => format!("LOAD_INPUT R({})", dest),
Instruction::LoadContext { dest } => format!("LOAD_CONTEXT R({})", dest),
Instruction::LoadMetadata { dest } => format!("LOAD_METADATA R({})", dest),
Instruction::Move { dest, src } => format!("MOVE R({}) R({})", dest, src),
Instruction::Add { dest, left, right } => {
format!("ADD R({}) R({}) R({})", dest, left, right)
@@ -220,6 +222,9 @@ impl core::fmt::Display for Instruction {
}
Instruction::ArrayNew { dest } => format!("ARRAY_NEW R({})", dest),
Instruction::ArrayPush { arr, value } => format!("ARRAY_PUSH R({}) R({})", arr, value),
Instruction::ArrayPushDefined { arr, value } => {
format!("ARRAY_PUSH_DEFINED R({}) R({})", arr, value)
}
Instruction::ArrayCreate { params_index } => {
format!("ARRAY_CREATE P({})", params_index)
}
@@ -247,6 +252,12 @@ impl core::fmt::Display for Instruction {
};
format!("{} R({})", name, register)
}
Instruction::ReturnUndefinedIfNotTrue { condition } => {
format!("RETURN_UNDEFINED_IF_NOT_TRUE R({})", condition)
}
Instruction::CoalesceUndefinedToNull { register } => {
format!("COALESCE_UNDEF_TO_NULL R({})", register)
}
Instruction::LoopStart { params_index } => {
format!("LOOP_START P({})", params_index)
}

View File

@@ -54,6 +54,16 @@ pub enum Instruction {
dest: u8,
},
/// Load host-supplied context value into register
LoadContext {
dest: u8,
},
/// Load program metadata value into register
LoadMetadata {
dest: u8,
},
/// Move value from one register to another
Move {
dest: u8,
@@ -206,6 +216,16 @@ pub enum Instruction {
value: u8,
},
/// Push element to array, but skip if the value is undefined.
///
/// Used by Azure Policy's `field('alias[*].property')` wildcard collection
/// so that absent nested properties are excluded from the collected array
/// rather than producing undefined entries.
ArrayPushDefined {
arr: u8,
value: u8,
},
/// Create array from registers - returns undefined if any element is undefined
ArrayCreate {
/// Index into program's instruction_data.array_create_params table
@@ -254,6 +274,25 @@ pub enum Instruction {
mode: GuardMode,
},
/// Return undefined immediately when the condition register is not exactly
/// `Bool(true)`. Any other value — including `false`, `Undefined`, `Null`,
/// numbers, strings, etc. — causes an immediate return of `Undefined`.
///
/// This is used by Azure Policy compilation to model "condition does not match"
/// without treating it as a VM assertion failure.
ReturnUndefinedIfNotTrue {
condition: u8,
},
/// Replace Undefined with Null in a register.
///
/// Azure Policy treats missing fields as null rather than undefined.
/// This instruction prevents the RVM's undefined-propagation from
/// short-circuiting subsequent builtin calls.
CoalesceUndefinedToNull {
register: u8,
},
/// Start a loop over a collection with specified semantics - uses parameter table
LoopStart {
/// Index into program's instruction_data.loop_params table

View File

@@ -307,6 +307,14 @@ fn format_instruction_readable(
let base = format!("{}LoadInput r{} ← input", indent, dest);
align_comment(&base, "Load global input document", config.comment_column)
}
Instruction::LoadContext { dest } => {
let base = format!("{}LoadContext r{} ← context", indent, dest);
align_comment(&base, "Load evaluation context", config.comment_column)
}
Instruction::LoadMetadata { dest } => {
let base = format!("{}LoadMetadata r{} ← metadata", indent, dest);
align_comment(&base, "Load program metadata", config.comment_column)
}
Instruction::Move { dest, src } => {
let base = format!("{}Move r{} ← r{}", indent, dest, src);
let comment = format!("Copy value from r{} to r{}", src, dest);
@@ -565,6 +573,11 @@ fn format_instruction_readable(
let comment = format!("Append r{} to array r{}", value, arr);
align_comment(&base, &comment, config.comment_column)
}
Instruction::ArrayPushDefined { arr, value } => {
let base = format!("{}ArrayPushDef r{}.push(r{})", indent, arr, value);
let comment = format!("Append r{} to array r{} (skip if undefined)", value, arr);
align_comment(&base, &comment, config.comment_column)
}
Instruction::ArrayCreate { params_index } => instruction_data
.get_array_create_params(params_index)
.map_or_else(
@@ -658,6 +671,25 @@ fn format_instruction_readable(
};
align_comment(&keyword, &comment, config.comment_column)
}
Instruction::ReturnUndefinedIfNotTrue { condition } => {
let base = format!(
"{}ReturnUndefinedIfNotTrue if r{} != true return undefined",
indent, condition
);
let comment = format!(
"Return undefined unless r{} is exactly boolean true",
condition
);
align_comment(&base, &comment, config.comment_column)
}
Instruction::CoalesceUndefinedToNull { register } => {
let base = format!(
"{}CoalesceUndefinedToNull r{} = null if undefined",
indent, register
);
let comment = format!("Azure Policy: absent field → null (r{})", register);
align_comment(&base, &comment, config.comment_column)
}
Instruction::LoopStart { params_index } => {
instruction_data.get_loop_params(params_index).map_or_else(
|| {
@@ -959,6 +991,8 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
Instruction::LoadBool { .. } => "LOAD_BOOL",
Instruction::LoadData { .. } => "LOAD_DATA",
Instruction::LoadInput { .. } => "LOAD_INPUT",
Instruction::LoadContext { .. } => "LOAD_CONTEXT",
Instruction::LoadMetadata { .. } => "LOAD_METADATA",
Instruction::Move { .. } => "MOVE",
Instruction::Add { .. } => "ADD",
Instruction::Sub { .. } => "SUB",
@@ -984,6 +1018,7 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
Instruction::IndexLiteral { .. } => "INDEX_LIT",
Instruction::ArrayNew { .. } => "ARRAY_NEW",
Instruction::ArrayPush { .. } => "ARRAY_PUSH",
Instruction::ArrayPushDefined { .. } => "ARRAY_PUSH_DEF",
Instruction::ArrayCreate { .. } => "ARRAY_CREATE",
Instruction::SetNew { .. } => "SET_NEW",
Instruction::SetAdd { .. } => "SET_ADD",
@@ -996,6 +1031,8 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
crate::rvm::instructions::GuardMode::Condition => "ASSERT",
crate::rvm::instructions::GuardMode::NotUndefined => "ASSERT_NOT_UNDEF",
},
Instruction::ReturnUndefinedIfNotTrue { .. } => "RET_UNDEF_IF_NOT_TRUE",
Instruction::CoalesceUndefinedToNull { .. } => "COALESCE_UNDEF_TO_NULL",
Instruction::LoopStart { .. } => "LOOP_START",
Instruction::LoopNext { .. } => "LOOP_NEXT",
Instruction::CallRule { .. } => "CALL_RULE",
@@ -1024,6 +1061,12 @@ fn format_operation_compact(
Instruction::LoadInput { dest } => {
format!("{}r{} ← input", indent, dest)
}
Instruction::LoadContext { dest } => {
format!("{}r{} ← context", indent, dest)
}
Instruction::LoadMetadata { dest } => {
format!("{}r{} ← metadata", indent, dest)
}
Instruction::LoadData { dest } => {
format!("{}r{} ← data", indent, dest)
}

View File

@@ -31,6 +31,8 @@ pub fn parse_instruction(text: &str) -> Result<Instruction> {
"LoadBool" => parse_load_bool(params_text),
"LoadData" => parse_load_data(params_text),
"LoadInput" => parse_load_input(params_text),
"LoadContext" => parse_load_context(params_text),
"LoadMetadata" => parse_load_metadata(params_text),
"Move" => parse_move(params_text),
"Add" => parse_add(params_text),
"Sub" => parse_sub(params_text),
@@ -59,6 +61,7 @@ pub fn parse_instruction(text: &str) -> Result<Instruction> {
"ArrayCreate" => parse_array_create(params_text),
"SetCreate" => parse_set_create(params_text),
"ArrayPush" => parse_array_push(params_text),
"ArrayPushDefined" => parse_array_push_defined(params_text),
"SetNew" => parse_set_new(params_text),
"SetAdd" => parse_set_add(params_text),
"Contains" => parse_contains(params_text),
@@ -78,6 +81,8 @@ pub fn parse_instruction(text: &str) -> Result<Instruction> {
"ComprehensionAdd" => parse_comprehension_add(params_text),
"ComprehensionBegin" => parse_comprehension_start(params_text),
"ComprehensionYield" => parse_comprehension_add(params_text),
"ReturnUndefinedIfNotTrue" => parse_return_undefined_if_not_true(params_text),
"CoalesceUndefinedToNull" => parse_coalesce_undefined_to_null(params_text),
_ => bail!("Unknown instruction: {}", name),
}
} else {
@@ -414,6 +419,16 @@ fn parse_array_push(params_text: &str) -> Result<Instruction> {
})
}
fn parse_array_push_defined(params_text: &str) -> Result<Instruction> {
let params = parse_params(params_text)?;
let arr = get_param_u16(&params, "arr")?;
let value = get_param_u16(&params, "value")?;
Ok(Instruction::ArrayPushDefined {
arr: arr.try_into().unwrap(),
value: value.try_into().unwrap(),
})
}
fn parse_array_create(params_text: &str) -> Result<Instruction> {
let params = parse_params(params_text)?;
let params_index = get_param_u16(&params, "params_index")?;
@@ -555,6 +570,22 @@ fn parse_load_input(params_text: &str) -> Result<Instruction> {
})
}
fn parse_load_context(params_text: &str) -> Result<Instruction> {
let params = parse_params(params_text)?;
let dest = get_param_u16(&params, "dest")?;
Ok(Instruction::LoadContext {
dest: dest.try_into().unwrap(),
})
}
fn parse_load_metadata(params_text: &str) -> Result<Instruction> {
let params = parse_params(params_text)?;
let dest = get_param_u16(&params, "dest")?;
Ok(Instruction::LoadMetadata {
dest: dest.try_into().unwrap(),
})
}
fn parse_mod(params_text: &str) -> Result<Instruction> {
let params = parse_params(params_text)?;
let dest = get_param_u16(&params, "dest")?;
@@ -660,3 +691,19 @@ fn parse_comprehension_add(params_text: &str) -> Result<Instruction> {
key_reg,
})
}
fn parse_return_undefined_if_not_true(params_text: &str) -> Result<Instruction> {
let params = parse_params(params_text)?;
let condition = get_param_u16(&params, "condition")?;
Ok(Instruction::ReturnUndefinedIfNotTrue {
condition: condition.try_into().unwrap(),
})
}
fn parse_coalesce_undefined_to_null(params_text: &str) -> Result<Instruction> {
let params = parse_params(params_text)?;
let register = get_param_u16(&params, "register")?;
Ok(Instruction::CoalesceUndefinedToNull {
register: register.try_into().unwrap(),
})
}

View File

@@ -83,6 +83,12 @@ mod tests {
data: Option<crate::Value>,
#[serde(default)]
input: Option<crate::Value>,
#[serde(default)]
context: Option<crate::Value>,
#[serde(default)]
metadata_language: Option<String>,
#[serde(default)]
metadata_annotations: Option<BTreeMap<String, crate::Value>>,
literals: Vec<crate::Value>,
#[serde(default)]
rule_infos: Vec<RuleInfoSpec>,
@@ -266,6 +272,9 @@ mod tests {
instruction_params: Option<InstructionParamsSpec>,
data: Option<Value>,
input: Option<Value>,
context: Option<Value>,
metadata_language: Option<String>,
metadata_annotations: Option<BTreeMap<String, Value>>,
max_instructions: Option<usize>,
host_await_responses: Option<Vec<HostAwaitResponseSpec>>,
host_await_responses_run_to_completion: Option<Vec<HostAwaitResponseSpec>>,
@@ -285,6 +294,12 @@ mod tests {
None
};
let processed_context = if let Some(ref context_value) = context {
Some(process_value(context_value)?)
} else {
None
};
let processed_rule_tree = if let Some(ref tree_value) = rule_tree {
Some(process_value(tree_value)?)
} else {
@@ -631,6 +646,21 @@ mod tests {
program.max_rule_window_size = 255;
program.dispatch_window_size = 50;
// Recompute derived flags since instructions were assigned directly
// (bypassing add_instruction which normally tracks has_host_await)
program.recompute_host_await_presence();
// Set metadata if provided
if let Some(lang) = metadata_language {
program.metadata.language = lang;
}
if let Some(annotations) = metadata_annotations {
program.metadata.annotations = annotations
.into_iter()
.map(|(key, value)| process_value(&value).map(|processed| (key, processed)))
.collect::<anyhow::Result<_>>()?;
}
// Initialize resolved builtins if we have builtin info
if !program.builtin_info_table.is_empty() {
if let Err(e) = program.initialize_resolved_builtins() {
@@ -664,6 +694,10 @@ mod tests {
vm.set_input(input_value);
}
if let Some(context_value) = processed_context.clone() {
vm.set_context(context_value);
}
if let Some(limit) = max_instructions {
vm.set_max_instructions(limit);
}
@@ -931,6 +965,9 @@ mod tests {
test_case.instruction_params.clone(),
test_case.data.clone(),
test_case.input.clone(),
test_case.context.clone(),
test_case.metadata_language.clone(),
test_case.metadata_annotations.clone(),
test_case.max_instructions,
test_case.host_await_responses.clone(),
test_case.host_await_responses_run_to_completion.clone(),

View File

@@ -312,7 +312,7 @@ impl RegoVM {
*current_item =
Some(self.get_register(comprehension_context.value_reg)?.clone());
}
IterationState::Array { .. } => {}
IterationState::Array { .. } | IterationState::Single { .. } => {}
}
iter_state.advance();
@@ -468,7 +468,7 @@ impl RegoVM {
} => {
*current_item = Some(iteration_value.clone());
}
IterationState::Array { .. } => {}
IterationState::Array { .. } | IterationState::Single { .. } => {}
}
iter_state.advance();
@@ -599,7 +599,7 @@ impl RegoVM {
} => {
*current_item = Some(self.get_register(value_reg)?.clone());
}
IterationState::Array { .. } => {}
IterationState::Array { .. } | IterationState::Single { .. } => {}
}
Ok(())

View File

@@ -41,6 +41,13 @@ pub enum IterationState {
current_item: Option<Value>,
first_iteration: bool,
},
/// Virtual single-element iteration for non-collection values.
/// Used by Azure Policy's `[*]` on scalar/null fields: presents a single
/// "virtual" element to iterate over, which is always `Null` regardless
/// of the underlying source value.
Single {
consumed: bool,
},
}
impl IterationState {
@@ -59,6 +66,11 @@ impl IterationState {
} => {
*first_iteration = false;
}
Self::Single {
ref mut consumed, ..
} => {
*consumed = true;
}
}
}
}

View File

@@ -72,6 +72,14 @@ impl RegoVM {
self.set_register(dest, self.input.clone())?;
Ok(InstructionOutcome::Continue)
}
LoadContext { dest } => {
self.set_register(dest, self.context.clone())?;
Ok(InstructionOutcome::Continue)
}
LoadMetadata { dest } => {
self.set_register(dest, self.metadata_value.clone())?;
Ok(InstructionOutcome::Continue)
}
Move { dest, src } => {
let value = self.get_register(src)?.clone();
self.set_register(dest, value)?;
@@ -351,6 +359,21 @@ impl RegoVM {
self.handle_condition(passed)?;
Ok(InstructionOutcome::Continue)
}
ReturnUndefinedIfNotTrue { condition } => {
let value = self.get_register(condition)?;
if matches!(value, Value::Bool(true)) {
Ok(InstructionOutcome::Continue)
} else {
Ok(InstructionOutcome::Return(Value::Undefined))
}
}
CoalesceUndefinedToNull { register } => {
let value = self.get_register(register)?;
if matches!(value, Value::Undefined) {
self.set_register(register, Value::Null)?;
}
Ok(InstructionOutcome::Continue)
}
other => self.execute_call_instruction(program, other),
}
}
@@ -585,6 +608,33 @@ impl RegoVM {
}
Ok(InstructionOutcome::Continue)
}
ArrayPushDefined { arr, value } => {
// Skip undefined values — matches Azure Policy's
// `field('alias[*].property')` collection semantics where
// absent nested properties are excluded from the collected
// array.
if self.get_register(value)? == &Value::Undefined {
return Ok(InstructionOutcome::Continue);
}
let value_to_push = self.get_register(value)?.clone();
let mut arr_value = self.take_register(arr)?;
if let Ok(arr_mut) = arr_value.as_array_mut() {
arr_mut.push(value_to_push);
self.set_register(arr, arr_value)?;
} else {
let offending = arr_value.clone();
self.set_register(arr, arr_value)?;
return Err(VmError::RegisterNotArray {
register: arr,
value: offending,
pc: self.pc,
});
}
Ok(InstructionOutcome::Continue)
}
ArrayCreate { params_index } => {
if let Some(params) = program
.instruction_data

View File

@@ -10,7 +10,12 @@ use super::execution_model::{ExecutionFrame, ExecutionMode, FrameKind};
use super::machine::RegoVM;
/// Result for a loop over a non-iterable value (null, string, number, bool, Undefined).
/// `Every` over empty is vacuously `true`.
/// In standard Rego mode, this helper is used for all loop modes when the
/// collection operand is not iterable: `Every` over empty is vacuously `true`,
/// and `Any`/`ForEach` over empty are `false`.
/// In Azure Policy mode, this helper is still used for `Any`/`ForEach` when an
/// object or other non-collection is encountered and short-circuits to `false`;
/// `Every` with virtual elements is handled via a different code path.
#[inline]
const fn non_collection_result(mode: &LoopMode) -> Value {
match *mode {
@@ -437,15 +442,29 @@ impl RegoVM {
}))
}
Value::Object(ref obj) => {
if obj.is_empty() {
self.handle_empty_collection(mode, params.result_reg, params.loop_end)?;
return Ok(None);
if self.virtual_element_on_non_collection {
// Azure Policy: `[*]` expects an array. Objects are
// treated as non-collections — virtual element for Every
// mode, immediate false for Any/ForEach.
if *mode == LoopMode::Every {
Ok(Some(IterationState::Single { consumed: false }))
} else {
let result = non_collection_result(mode);
self.set_register(params.result_reg, result)?;
self.pc = usize::from(params.loop_end).saturating_sub(1);
Ok(None)
}
} else {
if obj.is_empty() {
self.handle_empty_collection(mode, params.result_reg, params.loop_end)?;
return Ok(None);
}
Ok(Some(IterationState::Object {
obj: obj.clone(),
current_key: None,
first_iteration: true,
}))
}
Ok(Some(IterationState::Object {
obj: obj.clone(),
current_key: None,
first_iteration: true,
}))
}
Value::Set(ref set) => {
if set.is_empty() {
@@ -459,10 +478,17 @@ impl RegoVM {
}))
}
_ => {
let result = non_collection_result(mode);
self.set_register(params.result_reg, result)?;
self.pc = usize::from(params.loop_end).saturating_sub(1);
Ok(None)
if self.virtual_element_on_non_collection && *mode == LoopMode::Every {
// Azure Policy: allOf [*] on non-collection iterates once
// over a virtual null element.
Ok(Some(IterationState::Single { consumed: false }))
} else {
// Standard Rego or count/forEach: non-collection → immediate result.
let result = non_collection_result(mode);
self.set_register(params.result_reg, result)?;
self.pc = usize::from(params.loop_end).saturating_sub(1);
Ok(None)
}
}
}
}
@@ -576,6 +602,20 @@ impl RegoVM {
Ok(false)
}
}
IterationState::Single { ref consumed } => {
if *consumed {
Ok(false)
} else {
// Virtual single element: key=0, value=Null.
// Sub-field accesses on Null produce Undefined, which is
// what Azure Policy expects for missing/non-array [*].
if key_reg != value_reg {
self.set_register(key_reg, Value::from(0))?;
}
self.set_register(value_reg, Value::Null)?;
Ok(true)
}
}
}
}

View File

@@ -49,6 +49,9 @@ pub struct RegoVM {
/// Global input object
pub(super) input: Value,
/// Evaluation context: host-supplied ambient data available via LoadContext
pub(super) context: Value,
/// Loop execution stack
/// Note: Loops are either at the outermost level (rule body) or within the topmost comprehension.
/// Loops never contain comprehensions - it's always the other way around.
@@ -136,6 +139,20 @@ pub struct RegoVM {
/// Cached args Vec for builtin calls (avoids Vec allocation per call)
pub(super) cached_builtin_args: Vec<Value>,
/// When `true`, a loop over a value that is not treated as a collection
/// (null, strings, numbers, objects, and similar non-array values) uses
/// Azure Policy-compatible semantics. `Every` behaves as if iterating
/// over a single virtual element whose value is `Null`, instead of being
/// vacuously `true` over an empty collection. This matches Azure Policy
/// semantics where `field[*]` on a non-array value produces a single
/// `Null` element (which typically causes the condition to evaluate to
/// `false`). Automatically set from `program.metadata.language`.
pub(super) virtual_element_on_non_collection: bool,
/// Cached `Value` representation of `program.metadata`, computed once in
/// `load_program()` and reused by `LoadMetadata` instructions.
pub(super) metadata_value: Value,
}
impl Default for RegoVM {
@@ -157,6 +174,7 @@ impl RegoVM {
rule_cache: Vec::new(),
data: Value::Null,
input: Value::Null,
context: Value::Undefined,
loop_stack: Vec::new(),
call_rule_stack: Vec::new(),
register_stack: Vec::new(),
@@ -182,6 +200,8 @@ impl RegoVM {
dummy_span: None,
dummy_exprs: Vec::new(),
cached_builtin_args: Vec::new(),
virtual_element_on_non_collection: false,
metadata_value: Value::Undefined,
}
}
@@ -210,6 +230,13 @@ impl RegoVM {
// Set PC to main entry point
self.pc = usize::try_from(program.main_entry_point).unwrap_or(0);
self.executed_instructions = 0; // Reset instruction counter
// Azure Policy: loop over non-collection iterates a virtual Null element
// (instead of vacuously succeeding over an empty collection).
self.virtual_element_on_non_collection = program.metadata.language == "azure_policy";
// Cache the metadata as a Value for LoadMetadata instructions
self.metadata_value = program.metadata.to_value();
}
/// Set the compiled policy for default rule evaluation
@@ -246,6 +273,11 @@ impl RegoVM {
self.input = input;
}
/// Set the evaluation context (host-supplied ambient data)
pub fn set_context(&mut self, context: Value) {
self.context = context;
}
/// Get the number of entry points available
pub fn get_entry_point_count(&self) -> usize {
self.program.entry_points.len()
@@ -505,8 +537,8 @@ impl RegoVM {
}
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
fn map_limit_error(&self, err: LimitError) -> VmError {
match err {
pub(super) fn memory_check(&mut self) -> Result<()> {
limits::check_memory_limit_if_needed().map_err(|err| match err {
LimitError::MemoryLimitExceeded { usage, limit } => VmError::MemoryLimitExceeded {
usage,
limit,
@@ -516,12 +548,7 @@ impl RegoVM {
message: format!("unexpected limit error: {other}"),
pc: self.pc,
},
}
}
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
pub(super) fn memory_check(&mut self) -> Result<()> {
limits::check_memory_limit_if_needed().map_err(|err| self.map_limit_error(err))
})
}
#[cfg(any(miri, not(feature = "allocator-memory-limits")))]

View File

@@ -0,0 +1,92 @@
# ArrayPushDefined instruction test suite
#
# Like ArrayPush but silently skips Undefined values. Used by the Azure Policy
# compiler when collecting `field('alias[*].property')` results: absent nested
# properties produce Undefined and should be excluded from the collected array.
#
# Semantics:
# - If value register is Undefined → no-op (skip).
# - If value register is any other value (including Null) → push to array.
# - If arr register is not an array → error.
cases:
# =========================================================================
# Undefined values are skipped
# =========================================================================
- note: push_undefined_is_skipped
description: Pushing an undefined value leaves the array unchanged
literals: []
instructions:
- "ArrayNew { dest: 0 }"
# Register 1 is implicitly Undefined
- "ArrayPushDefined { arr: 0, value: 1 }"
- "Return { value: 0 }"
want_result: []
- note: push_undefined_among_defined
description: Only defined values are collected; undefined ones are silently dropped
literals:
- 10
- 20
instructions:
- "ArrayNew { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "ArrayPushDefined { arr: 0, value: 1 }"
# Register 2 is Undefined — should be skipped
- "ArrayPushDefined { arr: 0, value: 2 }"
- "Load { dest: 3, literal_idx: 1 }"
- "ArrayPushDefined { arr: 0, value: 3 }"
- "Return { value: 0 }"
want_result: [10, 20]
# =========================================================================
# Null and other values are kept
# =========================================================================
- note: push_null_is_kept
description: Null is not undefined — it is pushed to the array
literals: []
instructions:
- "ArrayNew { dest: 0 }"
- "LoadNull { dest: 1 }"
- "ArrayPushDefined { arr: 0, value: 1 }"
- "Return { value: 0 }"
want_result: [null]
- note: push_bool_is_kept
description: Boolean value is pushed normally
literals:
- true
instructions:
- "ArrayNew { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "ArrayPushDefined { arr: 0, value: 1 }"
- "Return { value: 0 }"
want_result: [true]
- note: push_string_is_kept
description: String value is pushed normally
literals:
- "hello"
instructions:
- "ArrayNew { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "ArrayPushDefined { arr: 0, value: 1 }"
- "Return { value: 0 }"
want_result: ["hello"]
# =========================================================================
# Non-array target is an error
# =========================================================================
- note: push_to_non_array_errors
description: ArrayPushDefined on a non-array register produces an error
literals:
- 42
- 1
instructions:
- "Load { dest: 0, literal_idx: 0 }"
- "Load { dest: 1, literal_idx: 1 }"
- "ArrayPushDefined { arr: 0, value: 1 }"
want_error: "Register 0 does not contain an array"

View File

@@ -0,0 +1,194 @@
# CoalesceUndefinedToNull instruction test suite
#
# Used by the Azure Policy compiler to convert undefined (missing) field values
# to null before passing them to comparison operators. This prevents undefined
# propagation from short-circuiting subsequent builtins.
#
# Semantics:
# - If register == Undefined → replaces it with Null (in-place).
# - For any other value (null, bool, number, string, array, object) → no-op.
cases:
# =========================================================================
# Basic coalescing behavior
# =========================================================================
- note: undefined_becomes_null
description: Undefined register value is replaced with null
literals: []
instructions:
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: null
- note: null_unchanged
description: Null value is not modified
literals: []
instructions:
- "LoadNull { dest: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: null
- note: true_unchanged
description: Boolean true is not modified
literals: []
instructions:
- "LoadTrue { dest: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: true
- note: false_unchanged
description: Boolean false is not modified
literals: []
instructions:
- "LoadFalse { dest: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: false
- note: number_unchanged
description: Number value is not modified
literals:
- 42
instructions:
- "Load { dest: 0, literal_idx: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: 42
- note: string_unchanged
description: String value is not modified
literals:
- "hello"
instructions:
- "Load { dest: 0, literal_idx: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: "hello"
- note: empty_string_unchanged
description: Empty string is not modified (not confused with undefined)
literals:
- ""
instructions:
- "Load { dest: 0, literal_idx: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: ""
- note: array_unchanged
description: Array value is not modified
literals: []
instructions:
- "ArrayNew { dest: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: []
# =========================================================================
# Multiple coalesces — only undefined registers are affected
# =========================================================================
- note: selective_coalescing
description: Only undefined registers are coalesced; others remain unchanged
literals:
- 99
instructions:
- "Load { dest: 0, literal_idx: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "CoalesceUndefinedToNull { register: 1 }"
- "ArrayNew { dest: 2 }"
- "ArrayPush { arr: 2, value: 0 }"
- "ArrayPush { arr: 2, value: 1 }"
- "Return { value: 2 }"
want_result: [99, null]
# =========================================================================
# Interaction with ReturnUndefinedIfNotTrue
# =========================================================================
- note: coalesce_then_guard_null_returns_undefined
description: >
Coalescing undefined to null, then passing to ReturnUndefinedIfNotTrue
returns undefined (null is not true)
literals:
- "should not reach"
instructions:
- "CoalesceUndefinedToNull { register: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Return { value: 1 }"
want_result: "#undefined"
- note: coalesce_preserves_true_for_guard
description: >
Coalescing a true value is a no-op, so ReturnUndefinedIfNotTrue
continues execution
literals:
- "reached"
instructions:
- "LoadTrue { dest: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Return { value: 1 }"
want_result: "reached"
# =========================================================================
# Interaction with HostAwait (suspendable mode)
# =========================================================================
- note: coalesce_after_host_await_undefined_response
description: >
HostAwait returns undefined, CoalesceUndefinedToNull converts it to null
literals:
- "await-id"
- "arg"
instructions:
- "Load { dest: 1, literal_idx: 1 }"
- "Load { dest: 2, literal_idx: 0 }"
- "HostAwait { dest: 0, arg: 1, id: 2 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
host_await_responses:
- id: "await-id"
value: "#undefined"
want_result: null
- note: coalesce_after_host_await_value_response
description: >
HostAwait returns a real value, CoalesceUndefinedToNull is a no-op
literals:
- "await-id"
- "arg"
instructions:
- "Load { dest: 1, literal_idx: 1 }"
- "Load { dest: 2, literal_idx: 0 }"
- "HostAwait { dest: 0, arg: 1, id: 2 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
host_await_responses:
- id: "await-id"
value: "real-value"
want_result: "real-value"
- note: suspendable_coalesce_after_host_await
description: >
In suspendable mode: HostAwait suspends and resumes with value,
CoalesceUndefinedToNull is a no-op
literals:
- "await-id"
- "arg"
instructions:
- "Load { dest: 1, literal_idx: 1 }"
- "Load { dest: 2, literal_idx: 0 }"
- "HostAwait { dest: 0, arg: 1, id: 2 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
host_await_responses_suspendable:
- id: "await-id"
value: 42
ignore_run_to_completion_hostawait_failure: true
want_result: 42

View File

@@ -0,0 +1,389 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# LoadContext and LoadMetadata Test Suite
# Tests LoadContext and LoadMetadata instructions
# Covers: no context, populated context, context indexing, metadata fields,
# annotations, metadata round-trip, and interaction with other loads
cases:
# ── LoadContext ────────────────────────────────────────────────────────────
- note: load_context_undefined
description: LoadContext returns Undefined when no context is set
literals: []
instructions:
- "LoadContext { dest: 0 }"
- "Return { value: 0 }"
want_result: "#undefined"
- note: load_context_empty_object
description: LoadContext returns empty object
context: {}
literals: []
instructions:
- "LoadContext { dest: 0 }"
- "Return { value: 0 }"
want_result: {}
- note: load_context_string
description: LoadContext returns a string value
context: "hello world"
literals: []
instructions:
- "LoadContext { dest: 0 }"
- "Return { value: 0 }"
want_result: "hello world"
- note: load_context_number
description: LoadContext returns a numeric value
context: 42
literals: []
instructions:
- "LoadContext { dest: 0 }"
- "Return { value: 0 }"
want_result: 42
- note: load_context_boolean
description: LoadContext returns a boolean value
context: true
literals: []
instructions:
- "LoadContext { dest: 0 }"
- "Return { value: 0 }"
want_result: true
- note: load_context_with_values
description: LoadContext returns a populated object
context:
api_version: "2021-06-01"
tenant_id: "abc-123"
literals: []
instructions:
- "LoadContext { dest: 0 }"
- "Return { value: 0 }"
want_result:
api_version: "2021-06-01"
tenant_id: "abc-123"
- note: load_context_nested
description: LoadContext returns deeply nested data
context:
level1:
level2:
value: "deep"
literals: []
instructions:
- "LoadContext { dest: 0 }"
- "Return { value: 0 }"
want_result:
level1:
level2:
value: "deep"
- note: load_context_and_index
description: LoadContext followed by indexing into context data
context:
api_version: "2021-06-01"
tenant_id: "abc-123"
literals:
- "api_version"
instructions:
- "LoadContext { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Return { value: 2 }"
want_result: "2021-06-01"
- note: load_context_index_missing_key
description: Loading context and indexing a missing key yields Undefined
context:
api_version: "2021-06-01"
literals:
- "nonexistent"
instructions:
- "LoadContext { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Return { value: 2 }"
want_result: "#undefined"
- note: load_context_deep_index
description: LoadContext with chained indexing into nested object
context:
request:
headers:
content_type: "application/json"
literals:
- "request"
- "headers"
- "content_type"
instructions:
- "LoadContext { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Load { dest: 3, literal_idx: 1 }"
- "Index { dest: 4, container: 2, key: 3 }"
- "Load { dest: 5, literal_idx: 2 }"
- "Index { dest: 6, container: 4, key: 5 }"
- "Return { value: 6 }"
want_result: "application/json"
- note: load_context_with_array
description: LoadContext with an array value
context:
tags:
- "production"
- "east-us"
literals:
- "tags"
instructions:
- "LoadContext { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Count { dest: 3, collection: 2 }"
- "Return { value: 3 }"
want_result: 2
- note: load_context_multiple_times
description: Loading context multiple times returns the same value
context:
value: 42
literals:
- "value"
instructions:
- "LoadContext { dest: 0 }"
- "LoadContext { dest: 1 }"
- "Eq { dest: 2, left: 0, right: 1 }"
- "Return { value: 2 }"
want_result: true
- note: load_context_independent_of_input
description: Context and Input are independent
input:
input_key: "from_input"
context:
context_key: "from_context"
literals:
- "context_key"
- "input_key"
- "from_context"
- "from_input"
instructions:
# Load context["context_key"] into r2
- "LoadContext { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
# Load input["input_key"] into r5
- "LoadInput { dest: 3 }"
- "Load { dest: 4, literal_idx: 1 }"
- "Index { dest: 5, container: 3, key: 4 }"
# Verify context value == "from_context"
- "Load { dest: 6, literal_idx: 2 }"
- "Eq { dest: 7, left: 2, right: 6 }"
# Verify input value == "from_input"
- "Load { dest: 8, literal_idx: 3 }"
- "Eq { dest: 9, left: 5, right: 8 }"
# Both must be true
- "And { dest: 10, left: 7, right: 9 }"
- "Return { value: 10 }"
want_result: true
- note: load_context_independent_of_data
description: Context and Data are independent
data:
data_key: "from_data"
context:
context_key: "from_context"
literals:
- "context_key"
instructions:
- "LoadContext { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Return { value: 2 }"
want_result: "from_context"
# ── LoadMetadata ────────────────────────────────────────────────────────────
- note: load_metadata_default
description: LoadMetadata returns object with metadata fields - check by indexing language
literals:
- "language"
instructions:
- "LoadMetadata { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Return { value: 2 }"
# Default language is empty string
want_result: ""
- note: load_metadata_language
description: LoadMetadata returns language field when set
metadata_language: "azure_policy"
literals:
- "language"
instructions:
- "LoadMetadata { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Return { value: 2 }"
want_result: "azure_policy"
- note: load_metadata_annotations_string
description: LoadMetadata includes string annotations
metadata_language: "azure_policy"
metadata_annotations:
policy_mode: "indexed"
policy_category: "Security"
literals:
- "annotations"
- "policy_mode"
instructions:
- "LoadMetadata { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Load { dest: 3, literal_idx: 1 }"
- "Index { dest: 4, container: 2, key: 3 }"
- "Return { value: 4 }"
want_result: "indexed"
- note: load_metadata_annotations_number
description: LoadMetadata includes numeric annotations
metadata_annotations:
max_retries: 3
literals:
- "annotations"
- "max_retries"
instructions:
- "LoadMetadata { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Load { dest: 3, literal_idx: 1 }"
- "Index { dest: 4, container: 2, key: 3 }"
- "Return { value: 4 }"
want_result: 3
- note: load_metadata_annotations_boolean
description: LoadMetadata includes boolean annotations
metadata_annotations:
is_audit: true
literals:
- "annotations"
- "is_audit"
instructions:
- "LoadMetadata { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Load { dest: 3, literal_idx: 1 }"
- "Index { dest: 4, container: 2, key: 3 }"
- "Return { value: 4 }"
want_result: true
- note: load_metadata_annotations_nested
description: LoadMetadata includes nested annotations (object)
metadata_annotations:
compliance:
framework: "nist"
controls:
- "AC-1"
- "AC-2"
literals:
- "annotations"
- "compliance"
- "framework"
instructions:
- "LoadMetadata { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Load { dest: 3, literal_idx: 1 }"
- "Index { dest: 4, container: 2, key: 3 }"
- "Load { dest: 5, literal_idx: 2 }"
- "Index { dest: 6, container: 4, key: 5 }"
- "Return { value: 6 }"
want_result: "nist"
- note: load_metadata_annotations_absent
description: LoadMetadata without annotations - indexing annotations gives Undefined
literals:
- "annotations"
instructions:
- "LoadMetadata { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "Return { value: 2 }"
want_result: "#undefined"
- note: load_metadata_multiple_times
description: Loading metadata multiple times returns same value
metadata_language: "rego"
literals:
- "language"
instructions:
- "LoadMetadata { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
- "LoadMetadata { dest: 3 }"
- "Load { dest: 4, literal_idx: 0 }"
- "Index { dest: 5, container: 3, key: 4 }"
- "Eq { dest: 6, left: 2, right: 5 }"
- "Return { value: 6 }"
want_result: true
# ── Combined LoadContext + LoadMetadata ──────────────────────────────────
- note: context_and_metadata_independent
description: Context and metadata are independent values
context:
env: "production"
metadata_language: "rego"
literals:
- "env"
- "language"
instructions:
- "LoadContext { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
# r2 = "production"
- "LoadMetadata { dest: 3 }"
- "Load { dest: 4, literal_idx: 1 }"
- "Index { dest: 5, container: 3, key: 4 }"
# r5 = "rego"
- "Ne { dest: 6, left: 2, right: 5 }"
- "Return { value: 6 }"
want_result: true
- note: all_four_loads
description: LoadData, LoadInput, LoadContext, LoadMetadata all coexist
data:
role: "admin"
input:
action: "read"
context:
region: "us-east"
metadata_language: "test"
literals:
- "role"
- "action"
- "region"
- "language"
instructions:
- "LoadData { dest: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Index { dest: 2, container: 0, key: 1 }"
# r2 = "admin"
- "LoadInput { dest: 3 }"
- "Load { dest: 4, literal_idx: 1 }"
- "Index { dest: 5, container: 3, key: 4 }"
# r5 = "read"
- "LoadContext { dest: 6 }"
- "Load { dest: 7, literal_idx: 2 }"
- "Index { dest: 8, container: 6, key: 7 }"
# r8 = "us-east"
- "LoadMetadata { dest: 9 }"
- "Load { dest: 10, literal_idx: 3 }"
- "Index { dest: 11, container: 9, key: 10 }"
# r11 = "test"
# Return context region as witness
- "Return { value: 8 }"
want_result: "us-east"

View File

@@ -0,0 +1,209 @@
# ReturnUndefinedIfNotTrue instruction test suite
#
# This instruction is used by the Azure Policy compiler to short-circuit
# evaluation when a policy condition does not match.
#
# Semantics:
# - If condition register == true (Bool), execution continues to the next instruction.
# - For ANY other value (false, null, undefined, numbers, strings, arrays, objects),
# the instruction immediately returns Value::Undefined.
cases:
# =========================================================================
# Basic behavior — true continues, everything else returns undefined
# =========================================================================
- note: true_continues
description: When condition is true, execution continues and the next value is returned
literals:
- "success"
instructions:
- "LoadTrue { dest: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Return { value: 1 }"
want_result: "success"
- note: false_returns_undefined
description: When condition is false, returns undefined immediately
literals: []
instructions:
- "LoadFalse { dest: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "LoadTrue { dest: 1 }"
- "Return { value: 1 }"
want_result: "#undefined"
- note: null_returns_undefined
description: When condition is null, returns undefined
literals: []
instructions:
- "LoadNull { dest: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "LoadTrue { dest: 1 }"
- "Return { value: 1 }"
want_result: "#undefined"
- note: undefined_returns_undefined
description: When condition is undefined (uninitialized register), returns undefined
literals: []
instructions:
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "LoadTrue { dest: 1 }"
- "Return { value: 1 }"
want_result: "#undefined"
- note: number_returns_undefined
description: Numbers are not boolean true — returns undefined
literals:
- 1
instructions:
- "Load { dest: 0, literal_idx: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "LoadTrue { dest: 1 }"
- "Return { value: 1 }"
want_result: "#undefined"
- note: string_returns_undefined
description: Non-empty strings are not boolean true — returns undefined
literals:
- "truthy"
instructions:
- "Load { dest: 0, literal_idx: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "LoadTrue { dest: 1 }"
- "Return { value: 1 }"
want_result: "#undefined"
# =========================================================================
# Multiple guards in sequence
# =========================================================================
- note: two_guards_both_true
description: Two consecutive guards — both true, execution reaches the end
literals:
- "final"
instructions:
- "LoadTrue { dest: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "LoadTrue { dest: 1 }"
- "ReturnUndefinedIfNotTrue { condition: 1 }"
- "Load { dest: 2, literal_idx: 0 }"
- "Return { value: 2 }"
want_result: "final"
- note: two_guards_first_false
description: First guard fails — immediately returns undefined, second guard not reached
literals:
- "final"
instructions:
- "LoadFalse { dest: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "LoadTrue { dest: 1 }"
- "ReturnUndefinedIfNotTrue { condition: 1 }"
- "Load { dest: 2, literal_idx: 0 }"
- "Return { value: 2 }"
want_result: "#undefined"
- note: two_guards_second_false
description: First guard passes, second fails — returns undefined
literals:
- "final"
instructions:
- "LoadTrue { dest: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "LoadFalse { dest: 1 }"
- "ReturnUndefinedIfNotTrue { condition: 1 }"
- "Load { dest: 2, literal_idx: 0 }"
- "Return { value: 2 }"
want_result: "#undefined"
# =========================================================================
# Combined with HostAwait (the auditIfNotExists pattern)
# =========================================================================
- note: host_await_then_guard_true
description: >
Simulates auditIfNotExists: HostAwait returns true (non-compliant),
guard passes, effect string is returned
literals:
- "auditIfNotExists"
- "azure.policy.audit_if_not_exists"
- "request"
instructions:
- "Load { dest: 1, literal_idx: 2 }"
- "Load { dest: 2, literal_idx: 1 }"
- "HostAwait { dest: 3, arg: 1, id: 2 }"
- "Load { dest: 4, literal_idx: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 3 }"
- "Return { value: 4 }"
host_await_responses:
- id: "azure.policy.audit_if_not_exists"
value: true
want_result: "auditIfNotExists"
- note: host_await_then_guard_false
description: >
Simulates auditIfNotExists: HostAwait returns false (compliant),
guard fails, returns undefined (no policy violation)
literals:
- "auditIfNotExists"
- "azure.policy.audit_if_not_exists"
- "request"
instructions:
- "Load { dest: 1, literal_idx: 2 }"
- "Load { dest: 2, literal_idx: 1 }"
- "HostAwait { dest: 3, arg: 1, id: 2 }"
- "Load { dest: 4, literal_idx: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 3 }"
- "Return { value: 4 }"
host_await_responses:
- id: "azure.policy.audit_if_not_exists"
value: false
want_result: "#undefined"
# =========================================================================
# Suspendable mode — HostAwait + ReturnUndefinedIfNotTrue
# =========================================================================
- note: suspendable_host_await_guard_true
description: >
In suspendable mode: HostAwait suspends, resumes with true,
ReturnUndefinedIfNotTrue continues, returns effect
literals:
- "deny"
- "await-id"
- "arg"
instructions:
- "Load { dest: 1, literal_idx: 2 }"
- "Load { dest: 2, literal_idx: 1 }"
- "HostAwait { dest: 3, arg: 1, id: 2 }"
- "Load { dest: 4, literal_idx: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 3 }"
- "Return { value: 4 }"
host_await_responses_suspendable:
- id: "await-id"
value: true
ignore_run_to_completion_hostawait_failure: true
want_result: "deny"
- note: suspendable_host_await_guard_false
description: >
In suspendable mode: HostAwait suspends, resumes with false,
ReturnUndefinedIfNotTrue returns undefined
literals:
- "deny"
- "await-id"
- "arg"
instructions:
- "Load { dest: 1, literal_idx: 2 }"
- "Load { dest: 2, literal_idx: 1 }"
- "HostAwait { dest: 3, arg: 1, id: 2 }"
- "Load { dest: 4, literal_idx: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 3 }"
- "Return { value: 4 }"
host_await_responses_suspendable:
- id: "await-id"
value: false
ignore_run_to_completion_hostawait_failure: true
want_result: "#undefined"

View File

@@ -96,7 +96,7 @@ cases:
want_error: "#undefined"
- note: logical_not_int
description: NOT with non-boolean defined operand should yield false
description: NOT with int operand treats non-boolean as truthy (result false)
example_rego: "not 42"
literals:
- 42