Skip to content

Commit 6603f15

Browse files
committed
feat(rvm): add context/metadata support, new instructions, and serialization v6
Extend the RVM infrastructure with evaluation context, program metadata, and several new instructions needed by language frontends beyond Rego. VM & instructions: - Add LoadContext / LoadMetadata instructions for accessing evaluation context and program metadata from within RVM programs - Add ArrayPushDefined (skip-undefined variant of ArrayPush) for wildcard alias collection where absent properties should be excluded - Add ReturnUndefinedIfNotTrue for early-return semantics without triggering VM assertion failures - Add CoalesceUndefinedToNull for languages where missing fields are null rather than undefined - Support IterationState::Single in loops and comprehensions for iterating over scalar values alongside collections Program metadata: - Extend ProgramMetadata with language identifier, typed annotations (BTreeMap<String, MetadataValue>), and to_value() conversion - Add MetadataValue enum (String, Bool, Integer, Float, Array, Object) with full serde support - Add has_host_await flag to Program with recompute_host_await_presence() Serialization: - Bump binary format version to 6 - Add JSON serialization for ProgramMetadata including annotations Compiler: - Track has_host_await during compilation - Minor import cleanups Test infrastructure: - Extend VM test harness with context, metadata_language, and metadata_annotations fields - Add instruction parser support for new instructions - Add assembly listing support (readable, tabular, compact formats) - Add 3 new YAML test suites: load_context_metadata, coalesce_undefined_to_null, return_undefined_if_not_true
1 parent 126cc12 commit 6603f15

20 files changed

Lines changed: 1451 additions & 34 deletions

src/languages/rego/compiler/core.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ impl<'a> Compiler<'a> {
259259
}
260260

261261
pub fn emit_instruction(&mut self, instruction: Instruction, span: &Span) {
262+
if matches!(instruction, Instruction::HostAwait { .. }) {
263+
self.program.has_host_await = true;
264+
}
262265
self.program.instructions.push(instruction);
263266

264267
let source_path = span.source.get_path().to_string();

src/languages/rego/compiler/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ pub use error::{CompilerError, Result, SpannedCompilerError};
2323
use crate::ast::ExprRef;
2424
use crate::lexer::Span;
2525
use crate::rvm::program::{Program, RuleType, SpanInfo};
26-
use crate::value::Value;
2726
use crate::CompiledPolicy;
27+
use crate::Value;
2828
use alloc::collections::{BTreeMap, BTreeSet};
2929
use alloc::string::String;
3030
use alloc::vec;

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: 37 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 evaluation context object into register
58+
LoadContext {
59+
dest: u8,
60+
},
61+
62+
/// Load program metadata object 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,23 @@ pub enum Instruction {
254274
mode: GuardMode,
255275
},
256276

277+
/// Return undefined immediately when condition is false or undefined.
278+
///
279+
/// This is used by Azure Policy compilation to model "condition does not match"
280+
/// without treating it as a VM assertion failure.
281+
ReturnUndefinedIfNotTrue {
282+
condition: u8,
283+
},
284+
285+
/// Replace Undefined with Null in a register.
286+
///
287+
/// Azure Policy treats missing fields as null rather than undefined.
288+
/// This instruction prevents the RVM's undefined-propagation from
289+
/// short-circuiting subsequent builtin calls.
290+
CoalesceUndefinedToNull {
291+
register: u8,
292+
},
293+
257294
/// Start a loop over a collection with specified semantics - uses parameter table
258295
LoopStart {
259296
/// Index into program's instruction_data.loop_params table

src/rvm/program/core.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ pub struct Program {
7070
/// Flag indicating that VirtualDataDocumentLookup instruction was used and runtime recursion checking is needed
7171
pub needs_runtime_recursion_check: bool,
7272

73+
/// Flag indicating whether this program contains any HostAwait instruction.
74+
/// Clients can use this to decide whether suspendable execution mode is required.
75+
#[serde(default)]
76+
pub has_host_await: bool,
77+
7378
/// Flag indicating that recompilation is needed due to partial deserialization failure
7479
/// This is set to true when the artifact section was successfully read but the extensible
7580
/// section failed to deserialize (e.g., due to version incompatibility)
@@ -85,7 +90,7 @@ pub struct Program {
8590

8691
impl Program {
8792
/// Current serialization format version
88-
pub const SERIALIZATION_VERSION: u32 = 5;
93+
pub const SERIALIZATION_VERSION: u32 = 6;
8994
/// Magic bytes to identify Regorus program files
9095
pub const MAGIC: [u8; 4] = *b"REGO";
9196
/// Maximum instructions supported (matches u16 jump targets)
@@ -122,10 +127,13 @@ impl Program {
122127
compiled_at: "unknown".to_string(),
123128
source_info: "unknown".to_string(),
124129
optimization_level: 0,
130+
language: String::new(),
131+
annotations: alloc::collections::BTreeMap::new(),
125132
},
126133
rule_tree: Value::new_object(),
127134
resolved_builtins: Vec::new(),
128135
needs_runtime_recursion_check: false,
136+
has_host_await: false,
129137
needs_recompilation: false,
130138
rego_v0: false, // Default to Rego v1
131139
}
@@ -330,10 +338,21 @@ impl Program {
330338

331339
/// Add instruction with optional span
332340
pub fn add_instruction(&mut self, instruction: Instruction, span: Option<SpanInfo>) {
341+
if matches!(instruction, Instruction::HostAwait { .. }) {
342+
self.has_host_await = true;
343+
}
333344
self.instructions.push(instruction);
334345
self.instruction_spans.push(span);
335346
}
336347

348+
/// Recompute whether HostAwait is present by scanning instructions.
349+
pub fn recompute_host_await_presence(&mut self) {
350+
self.has_host_await = self
351+
.instructions
352+
.iter()
353+
.any(|instruction| matches!(instruction, Instruction::HostAwait { .. }));
354+
}
355+
337356
/// Add literal value and return its index
338357
pub fn add_literal(&mut self, value: Value) -> usize {
339358
for (i, existing) in self.literals.iter().enumerate() {
@@ -408,6 +427,11 @@ impl Program {
408427
pub const fn is_fully_functional(&self) -> bool {
409428
!self.needs_recompilation
410429
}
430+
431+
/// Check whether HostAwait instructions are present in this program.
432+
pub const fn has_host_await(&self) -> bool {
433+
self.has_host_await
434+
}
411435
}
412436

413437
impl Default for Program {

src/rvm/program/listing.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,19 @@ pub fn generate_assembly_listing(program: &Program, config: &AssemblyListingConf
133133
program.metadata.optimization_level
134134
),
135135
);
136+
if !program.metadata.language.is_empty() {
137+
push_line(
138+
&mut output,
139+
format_args!("; language: {}", program.metadata.language),
140+
);
141+
}
142+
if !program.metadata.annotations.is_empty() {
143+
push_line(&mut output, format_args!("; annotations:"));
144+
for (key, value) in &program.metadata.annotations {
145+
let json = serde_json::to_string(value).unwrap_or_else(|_| "<invalid>".to_string());
146+
push_line(&mut output, format_args!("; {}: {}", key, json));
147+
}
148+
}
136149
}
137150

138151
push_line(&mut output, format_args!(";"));
@@ -294,6 +307,14 @@ fn format_instruction_readable(
294307
let base = format!("{}LoadInput r{} ← input", indent, dest);
295308
align_comment(&base, "Load global input document", config.comment_column)
296309
}
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+
}
297318
Instruction::Move { dest, src } => {
298319
let base = format!("{}Move r{} ← r{}", indent, dest, src);
299320
let comment = format!("Copy value from r{} to r{}", src, dest);
@@ -552,6 +573,11 @@ fn format_instruction_readable(
552573
let comment = format!("Append r{} to array r{}", value, arr);
553574
align_comment(&base, &comment, config.comment_column)
554575
}
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+
}
555581
Instruction::ArrayCreate { params_index } => instruction_data
556582
.get_array_create_params(params_index)
557583
.map_or_else(
@@ -645,6 +671,25 @@ fn format_instruction_readable(
645671
};
646672
align_comment(&keyword, &comment, config.comment_column)
647673
}
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+
}
648693
Instruction::LoopStart { params_index } => {
649694
instruction_data.get_loop_params(params_index).map_or_else(
650695
|| {
@@ -946,6 +991,8 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
946991
Instruction::LoadBool { .. } => "LOAD_BOOL",
947992
Instruction::LoadData { .. } => "LOAD_DATA",
948993
Instruction::LoadInput { .. } => "LOAD_INPUT",
994+
Instruction::LoadContext { .. } => "LOAD_CONTEXT",
995+
Instruction::LoadMetadata { .. } => "LOAD_METADATA",
949996
Instruction::Move { .. } => "MOVE",
950997
Instruction::Add { .. } => "ADD",
951998
Instruction::Sub { .. } => "SUB",
@@ -971,6 +1018,7 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
9711018
Instruction::IndexLiteral { .. } => "INDEX_LIT",
9721019
Instruction::ArrayNew { .. } => "ARRAY_NEW",
9731020
Instruction::ArrayPush { .. } => "ARRAY_PUSH",
1021+
Instruction::ArrayPushDefined { .. } => "ARRAY_PUSH_DEF",
9741022
Instruction::ArrayCreate { .. } => "ARRAY_CREATE",
9751023
Instruction::SetNew { .. } => "SET_NEW",
9761024
Instruction::SetAdd { .. } => "SET_ADD",
@@ -983,6 +1031,8 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
9831031
crate::rvm::instructions::GuardMode::Condition => "ASSERT",
9841032
crate::rvm::instructions::GuardMode::NotUndefined => "ASSERT_NOT_UNDEF",
9851033
},
1034+
Instruction::ReturnUndefinedIfNotTrue { .. } => "RET_UNDEF_IF_NOT_TRUE",
1035+
Instruction::CoalesceUndefinedToNull { .. } => "COALESCE_NULL",
9861036
Instruction::LoopStart { .. } => "LOOP_START",
9871037
Instruction::LoopNext { .. } => "LOOP_NEXT",
9881038
Instruction::CallRule { .. } => "CALL_RULE",
@@ -1011,6 +1061,12 @@ fn format_operation_compact(
10111061
Instruction::LoadInput { dest } => {
10121062
format!("{}r{} ← input", indent, dest)
10131063
}
1064+
Instruction::LoadContext { dest } => {
1065+
format!("{}r{} ← context", indent, dest)
1066+
}
1067+
Instruction::LoadMetadata { dest } => {
1068+
format!("{}r{} ← metadata", indent, dest)
1069+
}
10141070
Instruction::LoadData { dest } => {
10151071
format!("{}r{} ← data", indent, dest)
10161072
}

src/rvm/program/serialization/binary.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,13 @@ impl Program {
151151
}
152152

153153
match version {
154-
1..=3 => {
154+
1..=5 => {
155155
let mut program = Program::new();
156156
program.needs_recompilation = true;
157157
program.rego_v0 = Self::legacy_rego_v0(data, version).unwrap_or(false);
158158
Ok(DeserializationResult::Partial(program))
159159
}
160-
4 | 5 => {
160+
6 => {
161161
if data.len() < 29 {
162162
return Err("Data too short for header".to_string());
163163
}
@@ -281,15 +281,15 @@ impl Program {
281281
let version = Self::read_u32(data, 4).ok();
282282

283283
match version {
284-
Some(1..=5) => Ok(true),
284+
Some(1..=6) => Ok(true),
285285
_ => Ok(false),
286286
}
287287
}
288288

289289
fn legacy_rego_v0(data: &[u8], version: u32) -> Option<bool> {
290290
match version {
291291
1 => data.get(16).map(|value| *value != 0),
292-
2 | 3 => data.get(24).map(|value| *value != 0),
292+
2..=5 => data.get(24).map(|value| *value != 0),
293293
_ => None,
294294
}
295295
}

src/rvm/program/serialization/json.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ impl Program {
2424
"optimization_level": self.metadata.optimization_level,
2525
"rego_v0": self.rego_v0,
2626
"needs_runtime_recursion_check": self.needs_runtime_recursion_check,
27+
"has_host_await": self.has_host_await,
2728
"needs_recompilation": self.needs_recompilation
2829
},
2930
"program_structure": {
@@ -84,6 +85,27 @@ impl Program {
8485
.and_then(|v| v.as_u64())
8586
.and_then(|v| u8::try_from(v).ok())
8687
.unwrap_or(0);
88+
let language = metadata
89+
.get("language")
90+
.and_then(|v| v.as_str())
91+
.unwrap_or("")
92+
.to_string();
93+
let annotations: alloc::collections::BTreeMap<String, Value> = metadata
94+
.get("annotations")
95+
.and_then(|v| match *v {
96+
// Deserialize JSON annotations into Value map
97+
serde_json::Value::Object(ref map) => {
98+
let mut result = alloc::collections::BTreeMap::new();
99+
for (k, json_val) in map {
100+
if let Ok(val) = serde_json::from_value::<Value>(json_val.clone()) {
101+
result.insert(k.clone(), val);
102+
}
103+
}
104+
Some(result)
105+
}
106+
_ => None,
107+
})
108+
.unwrap_or_default();
87109
let rego_v0 = metadata
88110
.get("rego_v0")
89111
.and_then(|v| v.as_bool())
@@ -92,6 +114,10 @@ impl Program {
92114
.get("needs_runtime_recursion_check")
93115
.and_then(|v| v.as_bool())
94116
.unwrap_or(false);
117+
let has_host_await = metadata
118+
.get("has_host_await")
119+
.and_then(|v| v.as_bool())
120+
.unwrap_or(false);
95121
let needs_recompilation = metadata
96122
.get("needs_recompilation")
97123
.and_then(|v| v.as_bool())
@@ -183,14 +209,19 @@ impl Program {
183209
compiled_at,
184210
source_info,
185211
optimization_level,
212+
language,
213+
annotations,
186214
},
187215
rule_tree,
188216
resolved_builtins: Vec::new(),
189217
needs_runtime_recursion_check,
218+
has_host_await,
190219
needs_recompilation,
191220
rego_v0,
192221
};
193222

223+
program.recompute_host_await_presence();
224+
194225
if !program.builtin_info_table.is_empty() {
195226
let _ = program.initialize_resolved_builtins();
196227
}

0 commit comments

Comments
 (0)