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
+50
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