Skip to content

Commit e5ac9a2

Browse files
authored
feat(rvm): new instructions and loop semantics for Azure Policy support (microsoft#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.
1 parent 8f740e2 commit e5ac9a2

15 files changed

Lines changed: 1215 additions & 25 deletions

src/rvm/instructions/display.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ impl core::fmt::Display for Instruction {
143143
Instruction::LoadBool { dest, value } => format!("LOAD_BOOL R({}) {}", dest, value),
144144
Instruction::LoadData { dest } => format!("LOAD_DATA R({})", dest),
145145
Instruction::LoadInput { dest } => format!("LOAD_INPUT R({})", dest),
146+
Instruction::LoadContext { dest } => format!("LOAD_CONTEXT R({})", dest),
147+
Instruction::LoadMetadata { dest } => format!("LOAD_METADATA R({})", dest),
146148
Instruction::Move { dest, src } => format!("MOVE R({}) R({})", dest, src),
147149
Instruction::Add { dest, left, right } => {
148150
format!("ADD R({}) R({}) R({})", dest, left, right)
@@ -220,6 +222,9 @@ impl core::fmt::Display for Instruction {
220222
}
221223
Instruction::ArrayNew { dest } => format!("ARRAY_NEW R({})", dest),
222224
Instruction::ArrayPush { arr, value } => format!("ARRAY_PUSH R({}) R({})", arr, value),
225+
Instruction::ArrayPushDefined { arr, value } => {
226+
format!("ARRAY_PUSH_DEFINED R({}) R({})", arr, value)
227+
}
223228
Instruction::ArrayCreate { params_index } => {
224229
format!("ARRAY_CREATE P({})", params_index)
225230
}
@@ -247,6 +252,12 @@ impl core::fmt::Display for Instruction {
247252
};
248253
format!("{} R({})", name, register)
249254
}
255+
Instruction::ReturnUndefinedIfNotTrue { condition } => {
256+
format!("RETURN_UNDEFINED_IF_NOT_TRUE R({})", condition)
257+
}
258+
Instruction::CoalesceUndefinedToNull { register } => {
259+
format!("COALESCE_UNDEF_TO_NULL R({})", register)
260+
}
250261
Instruction::LoopStart { params_index } => {
251262
format!("LOOP_START P({})", params_index)
252263
}

src/rvm/instructions/mod.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ pub enum Instruction {
5454
dest: u8,
5555
},
5656

57+
/// Load host-supplied context value into register
58+
LoadContext {
59+
dest: u8,
60+
},
61+
62+
/// Load program metadata value into register
63+
LoadMetadata {
64+
dest: u8,
65+
},
66+
5767
/// Move value from one register to another
5868
Move {
5969
dest: u8,
@@ -206,6 +216,16 @@ pub enum Instruction {
206216
value: u8,
207217
},
208218

219+
/// Push element to array, but skip if the value is undefined.
220+
///
221+
/// Used by Azure Policy's `field('alias[*].property')` wildcard collection
222+
/// so that absent nested properties are excluded from the collected array
223+
/// rather than producing undefined entries.
224+
ArrayPushDefined {
225+
arr: u8,
226+
value: u8,
227+
},
228+
209229
/// Create array from registers - returns undefined if any element is undefined
210230
ArrayCreate {
211231
/// Index into program's instruction_data.array_create_params table
@@ -254,6 +274,25 @@ pub enum Instruction {
254274
mode: GuardMode,
255275
},
256276

277+
/// Return undefined immediately when the condition register is not exactly
278+
/// `Bool(true)`. Any other value — including `false`, `Undefined`, `Null`,
279+
/// numbers, strings, etc. — causes an immediate return of `Undefined`.
280+
///
281+
/// This is used by Azure Policy compilation to model "condition does not match"
282+
/// without treating it as a VM assertion failure.
283+
ReturnUndefinedIfNotTrue {
284+
condition: u8,
285+
},
286+
287+
/// Replace Undefined with Null in a register.
288+
///
289+
/// Azure Policy treats missing fields as null rather than undefined.
290+
/// This instruction prevents the RVM's undefined-propagation from
291+
/// short-circuiting subsequent builtin calls.
292+
CoalesceUndefinedToNull {
293+
register: u8,
294+
},
295+
257296
/// Start a loop over a collection with specified semantics - uses parameter table
258297
LoopStart {
259298
/// Index into program's instruction_data.loop_params table

src/rvm/program/listing.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,14 @@ fn format_instruction_readable(
307307
let base = format!("{}LoadInput r{} ← input", indent, dest);
308308
align_comment(&base, "Load global input document", config.comment_column)
309309
}
310+
Instruction::LoadContext { dest } => {
311+
let base = format!("{}LoadContext r{} ← context", indent, dest);
312+
align_comment(&base, "Load evaluation context", config.comment_column)
313+
}
314+
Instruction::LoadMetadata { dest } => {
315+
let base = format!("{}LoadMetadata r{} ← metadata", indent, dest);
316+
align_comment(&base, "Load program metadata", config.comment_column)
317+
}
310318
Instruction::Move { dest, src } => {
311319
let base = format!("{}Move r{} ← r{}", indent, dest, src);
312320
let comment = format!("Copy value from r{} to r{}", src, dest);
@@ -565,6 +573,11 @@ fn format_instruction_readable(
565573
let comment = format!("Append r{} to array r{}", value, arr);
566574
align_comment(&base, &comment, config.comment_column)
567575
}
576+
Instruction::ArrayPushDefined { arr, value } => {
577+
let base = format!("{}ArrayPushDef r{}.push(r{})", indent, arr, value);
578+
let comment = format!("Append r{} to array r{} (skip if undefined)", value, arr);
579+
align_comment(&base, &comment, config.comment_column)
580+
}
568581
Instruction::ArrayCreate { params_index } => instruction_data
569582
.get_array_create_params(params_index)
570583
.map_or_else(
@@ -658,6 +671,25 @@ fn format_instruction_readable(
658671
};
659672
align_comment(&keyword, &comment, config.comment_column)
660673
}
674+
Instruction::ReturnUndefinedIfNotTrue { condition } => {
675+
let base = format!(
676+
"{}ReturnUndefinedIfNotTrue if r{} != true return undefined",
677+
indent, condition
678+
);
679+
let comment = format!(
680+
"Return undefined unless r{} is exactly boolean true",
681+
condition
682+
);
683+
align_comment(&base, &comment, config.comment_column)
684+
}
685+
Instruction::CoalesceUndefinedToNull { register } => {
686+
let base = format!(
687+
"{}CoalesceUndefinedToNull r{} = null if undefined",
688+
indent, register
689+
);
690+
let comment = format!("Azure Policy: absent field → null (r{})", register);
691+
align_comment(&base, &comment, config.comment_column)
692+
}
661693
Instruction::LoopStart { params_index } => {
662694
instruction_data.get_loop_params(params_index).map_or_else(
663695
|| {
@@ -959,6 +991,8 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
959991
Instruction::LoadBool { .. } => "LOAD_BOOL",
960992
Instruction::LoadData { .. } => "LOAD_DATA",
961993
Instruction::LoadInput { .. } => "LOAD_INPUT",
994+
Instruction::LoadContext { .. } => "LOAD_CONTEXT",
995+
Instruction::LoadMetadata { .. } => "LOAD_METADATA",
962996
Instruction::Move { .. } => "MOVE",
963997
Instruction::Add { .. } => "ADD",
964998
Instruction::Sub { .. } => "SUB",
@@ -984,6 +1018,7 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
9841018
Instruction::IndexLiteral { .. } => "INDEX_LIT",
9851019
Instruction::ArrayNew { .. } => "ARRAY_NEW",
9861020
Instruction::ArrayPush { .. } => "ARRAY_PUSH",
1021+
Instruction::ArrayPushDefined { .. } => "ARRAY_PUSH_DEF",
9871022
Instruction::ArrayCreate { .. } => "ARRAY_CREATE",
9881023
Instruction::SetNew { .. } => "SET_NEW",
9891024
Instruction::SetAdd { .. } => "SET_ADD",
@@ -996,6 +1031,8 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
9961031
crate::rvm::instructions::GuardMode::Condition => "ASSERT",
9971032
crate::rvm::instructions::GuardMode::NotUndefined => "ASSERT_NOT_UNDEF",
9981033
},
1034+
Instruction::ReturnUndefinedIfNotTrue { .. } => "RET_UNDEF_IF_NOT_TRUE",
1035+
Instruction::CoalesceUndefinedToNull { .. } => "COALESCE_UNDEF_TO_NULL",
9991036
Instruction::LoopStart { .. } => "LOOP_START",
10001037
Instruction::LoopNext { .. } => "LOOP_NEXT",
10011038
Instruction::CallRule { .. } => "CALL_RULE",
@@ -1024,6 +1061,12 @@ fn format_operation_compact(
10241061
Instruction::LoadInput { dest } => {
10251062
format!("{}r{} ← input", indent, dest)
10261063
}
1064+
Instruction::LoadContext { dest } => {
1065+
format!("{}r{} ← context", indent, dest)
1066+
}
1067+
Instruction::LoadMetadata { dest } => {
1068+
format!("{}r{} ← metadata", indent, dest)
1069+
}
10271070
Instruction::LoadData { dest } => {
10281071
format!("{}r{} ← data", indent, dest)
10291072
}

src/rvm/tests/instruction_parser.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ pub fn parse_instruction(text: &str) -> Result<Instruction> {
3131
"LoadBool" => parse_load_bool(params_text),
3232
"LoadData" => parse_load_data(params_text),
3333
"LoadInput" => parse_load_input(params_text),
34+
"LoadContext" => parse_load_context(params_text),
35+
"LoadMetadata" => parse_load_metadata(params_text),
3436
"Move" => parse_move(params_text),
3537
"Add" => parse_add(params_text),
3638
"Sub" => parse_sub(params_text),
@@ -59,6 +61,7 @@ pub fn parse_instruction(text: &str) -> Result<Instruction> {
5961
"ArrayCreate" => parse_array_create(params_text),
6062
"SetCreate" => parse_set_create(params_text),
6163
"ArrayPush" => parse_array_push(params_text),
64+
"ArrayPushDefined" => parse_array_push_defined(params_text),
6265
"SetNew" => parse_set_new(params_text),
6366
"SetAdd" => parse_set_add(params_text),
6467
"Contains" => parse_contains(params_text),
@@ -78,6 +81,8 @@ pub fn parse_instruction(text: &str) -> Result<Instruction> {
7881
"ComprehensionAdd" => parse_comprehension_add(params_text),
7982
"ComprehensionBegin" => parse_comprehension_start(params_text),
8083
"ComprehensionYield" => parse_comprehension_add(params_text),
84+
"ReturnUndefinedIfNotTrue" => parse_return_undefined_if_not_true(params_text),
85+
"CoalesceUndefinedToNull" => parse_coalesce_undefined_to_null(params_text),
8186
_ => bail!("Unknown instruction: {}", name),
8287
}
8388
} else {
@@ -414,6 +419,16 @@ fn parse_array_push(params_text: &str) -> Result<Instruction> {
414419
})
415420
}
416421

422+
fn parse_array_push_defined(params_text: &str) -> Result<Instruction> {
423+
let params = parse_params(params_text)?;
424+
let arr = get_param_u16(&params, "arr")?;
425+
let value = get_param_u16(&params, "value")?;
426+
Ok(Instruction::ArrayPushDefined {
427+
arr: arr.try_into().unwrap(),
428+
value: value.try_into().unwrap(),
429+
})
430+
}
431+
417432
fn parse_array_create(params_text: &str) -> Result<Instruction> {
418433
let params = parse_params(params_text)?;
419434
let params_index = get_param_u16(&params, "params_index")?;
@@ -555,6 +570,22 @@ fn parse_load_input(params_text: &str) -> Result<Instruction> {
555570
})
556571
}
557572

573+
fn parse_load_context(params_text: &str) -> Result<Instruction> {
574+
let params = parse_params(params_text)?;
575+
let dest = get_param_u16(&params, "dest")?;
576+
Ok(Instruction::LoadContext {
577+
dest: dest.try_into().unwrap(),
578+
})
579+
}
580+
581+
fn parse_load_metadata(params_text: &str) -> Result<Instruction> {
582+
let params = parse_params(params_text)?;
583+
let dest = get_param_u16(&params, "dest")?;
584+
Ok(Instruction::LoadMetadata {
585+
dest: dest.try_into().unwrap(),
586+
})
587+
}
588+
558589
fn parse_mod(params_text: &str) -> Result<Instruction> {
559590
let params = parse_params(params_text)?;
560591
let dest = get_param_u16(&params, "dest")?;
@@ -660,3 +691,19 @@ fn parse_comprehension_add(params_text: &str) -> Result<Instruction> {
660691
key_reg,
661692
})
662693
}
694+
695+
fn parse_return_undefined_if_not_true(params_text: &str) -> Result<Instruction> {
696+
let params = parse_params(params_text)?;
697+
let condition = get_param_u16(&params, "condition")?;
698+
Ok(Instruction::ReturnUndefinedIfNotTrue {
699+
condition: condition.try_into().unwrap(),
700+
})
701+
}
702+
703+
fn parse_coalesce_undefined_to_null(params_text: &str) -> Result<Instruction> {
704+
let params = parse_params(params_text)?;
705+
let register = get_param_u16(&params, "register")?;
706+
Ok(Instruction::CoalesceUndefinedToNull {
707+
register: register.try_into().unwrap(),
708+
})
709+
}

src/rvm/tests/vm.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ mod tests {
8383
data: Option<crate::Value>,
8484
#[serde(default)]
8585
input: Option<crate::Value>,
86+
#[serde(default)]
87+
context: Option<crate::Value>,
88+
#[serde(default)]
89+
metadata_language: Option<String>,
90+
#[serde(default)]
91+
metadata_annotations: Option<BTreeMap<String, crate::Value>>,
8692
literals: Vec<crate::Value>,
8793
#[serde(default)]
8894
rule_infos: Vec<RuleInfoSpec>,
@@ -266,6 +272,9 @@ mod tests {
266272
instruction_params: Option<InstructionParamsSpec>,
267273
data: Option<Value>,
268274
input: Option<Value>,
275+
context: Option<Value>,
276+
metadata_language: Option<String>,
277+
metadata_annotations: Option<BTreeMap<String, Value>>,
269278
max_instructions: Option<usize>,
270279
host_await_responses: Option<Vec<HostAwaitResponseSpec>>,
271280
host_await_responses_run_to_completion: Option<Vec<HostAwaitResponseSpec>>,
@@ -285,6 +294,12 @@ mod tests {
285294
None
286295
};
287296

297+
let processed_context = if let Some(ref context_value) = context {
298+
Some(process_value(context_value)?)
299+
} else {
300+
None
301+
};
302+
288303
let processed_rule_tree = if let Some(ref tree_value) = rule_tree {
289304
Some(process_value(tree_value)?)
290305
} else {
@@ -631,6 +646,21 @@ mod tests {
631646
program.max_rule_window_size = 255;
632647
program.dispatch_window_size = 50;
633648

649+
// Recompute derived flags since instructions were assigned directly
650+
// (bypassing add_instruction which normally tracks has_host_await)
651+
program.recompute_host_await_presence();
652+
653+
// Set metadata if provided
654+
if let Some(lang) = metadata_language {
655+
program.metadata.language = lang;
656+
}
657+
if let Some(annotations) = metadata_annotations {
658+
program.metadata.annotations = annotations
659+
.into_iter()
660+
.map(|(key, value)| process_value(&value).map(|processed| (key, processed)))
661+
.collect::<anyhow::Result<_>>()?;
662+
}
663+
634664
// Initialize resolved builtins if we have builtin info
635665
if !program.builtin_info_table.is_empty() {
636666
if let Err(e) = program.initialize_resolved_builtins() {
@@ -664,6 +694,10 @@ mod tests {
664694
vm.set_input(input_value);
665695
}
666696

697+
if let Some(context_value) = processed_context.clone() {
698+
vm.set_context(context_value);
699+
}
700+
667701
if let Some(limit) = max_instructions {
668702
vm.set_max_instructions(limit);
669703
}
@@ -931,6 +965,9 @@ mod tests {
931965
test_case.instruction_params.clone(),
932966
test_case.data.clone(),
933967
test_case.input.clone(),
968+
test_case.context.clone(),
969+
test_case.metadata_language.clone(),
970+
test_case.metadata_annotations.clone(),
934971
test_case.max_instructions,
935972
test_case.host_await_responses.clone(),
936973
test_case.host_await_responses_run_to_completion.clone(),

src/rvm/vm/comprehension.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ impl RegoVM {
312312
*current_item =
313313
Some(self.get_register(comprehension_context.value_reg)?.clone());
314314
}
315-
IterationState::Array { .. } => {}
315+
IterationState::Array { .. } | IterationState::Single { .. } => {}
316316
}
317317

318318
iter_state.advance();
@@ -468,7 +468,7 @@ impl RegoVM {
468468
} => {
469469
*current_item = Some(iteration_value.clone());
470470
}
471-
IterationState::Array { .. } => {}
471+
IterationState::Array { .. } | IterationState::Single { .. } => {}
472472
}
473473

474474
iter_state.advance();
@@ -599,7 +599,7 @@ impl RegoVM {
599599
} => {
600600
*current_item = Some(self.get_register(value_reg)?.clone());
601601
}
602-
IterationState::Array { .. } => {}
602+
IterationState::Array { .. } | IterationState::Single { .. } => {}
603603
}
604604

605605
Ok(())

0 commit comments

Comments
 (0)