mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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:
committed by
GitHub
parent
8f740e2f6f
commit
e5ac9a2734
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(¶ms, "arr")?;
|
||||
let value = get_param_u16(¶ms, "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(¶ms, "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(¶ms, "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(¶ms, "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(¶ms, "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(¶ms, "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(¶ms, "register")?;
|
||||
Ok(Instruction::CoalesceUndefinedToNull {
|
||||
register: register.try_into().unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")))]
|
||||
|
||||
Reference in New Issue
Block a user