Files
regorus/src/rvm/instructions/mod.rs
T
Anand Krishnamoorthi 4d35744c4f feat(rvm): implement Azure Policy condition evaluation (#661)
Add VM support for Azure Policy's condition operators and allOf/anyOf
short-circuit logic, gated behind cfg(feature = "azure_policy").

Policy conditions (equals, contains, like, match, exists, and their
negations — 21 total) are encoded as a single PolicyCondition
instruction with a PolicyOp sub-opcode rather than bloating the
Instruction enum with 21 variants. The dispatch handles Azure Policy's
quirky comparison semantics: case-insensitive string comparison,
string↔number coercion, null vs undefined distinction, and element-wise
collection membership.

allOf/anyOf blocks use four instructions — LogicalBlockStart,
AllOfNext/AnyOfNext, and LogicalBlockEnd — that wire up a result
register and short-circuit on the first failing (allOf) or passing
(anyOf) child.

Helper functions for case-folded comparison, wildcard/glob matching, and
type coercion live in builtins::azure_policy::helpers.

Two YAML test suites (~2200 lines) exercise the full operator matrix and
the allOf/anyOf control flow.
2026-04-07 19:04:24 -05:00

506 lines
13 KiB
Rust

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
mod display;
mod params;
mod types;
pub use params::{
ArrayCreateParams, BuiltinCallParams, ChainedIndexParams, ComprehensionBeginParams,
FunctionCallParams, InstructionData, LoopStartParams, ObjectCreateParams, SetCreateParams,
VirtualDataDocumentLookupParams,
};
pub use types::{
ComprehensionMode, GuardMode, LiteralOrRegister, LogicalBlockMode, LoopMode, PolicyOp,
};
use serde::{Deserialize, Serialize};
/// RVM Instructions - simplified enum-based design
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Instruction {
/// Load literal value from literal table into register
Load {
dest: u8,
literal_idx: u16,
},
/// Load true value into register
LoadTrue {
dest: u8,
},
/// Load false value into register
LoadFalse {
dest: u8,
},
/// Load null value into register
LoadNull {
dest: u8,
},
/// Load boolean value into register
LoadBool {
dest: u8,
value: bool,
},
/// Load global data object into register
LoadData {
dest: u8,
},
/// Load global input object into register
LoadInput {
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,
src: u8,
},
/// Arithmetic operations
Add {
dest: u8,
left: u8,
right: u8,
},
Sub {
dest: u8,
left: u8,
right: u8,
},
Mul {
dest: u8,
left: u8,
right: u8,
},
Div {
dest: u8,
left: u8,
right: u8,
},
Mod {
dest: u8,
left: u8,
right: u8,
},
/// Comparison operations
Eq {
dest: u8,
left: u8,
right: u8,
},
Ne {
dest: u8,
left: u8,
right: u8,
},
Lt {
dest: u8,
left: u8,
right: u8,
},
Le {
dest: u8,
left: u8,
right: u8,
},
Gt {
dest: u8,
left: u8,
right: u8,
},
Ge {
dest: u8,
left: u8,
right: u8,
},
/// Logical operations
And {
dest: u8,
left: u8,
right: u8,
},
Or {
dest: u8,
left: u8,
right: u8,
},
Not {
dest: u8,
operand: u8,
},
/// Builtin function calls - optimized for builtin functions
BuiltinCall {
/// Index into program's instruction_data.builtin_call_params table
params_index: u16,
},
/// Suspend execution and yield control to the host
HostAwait {
/// Destination register to store the resume value
dest: u8,
/// Register containing the value to pass to the host
arg: u8,
/// Register containing a unique identifier for this await site
id: u8,
},
/// Function rule calls - for user-defined function rules
FunctionCall {
/// Index into program's instruction_data.function_call_params table
params_index: u16,
},
/// Return result
Return {
value: u8,
},
/// Set object field
ObjectSet {
obj: u8,
key: u8,
value: u8,
},
/// Create object with optimized field setting - uses parameter table
ObjectCreate {
/// Index into program's instruction_data.object_create_params table
params_index: u16,
},
/// Index into container (object, array, set)
Index {
dest: u8,
container: u8,
key: u8,
},
/// Index into container using literal key (optimization for Load + Index)
IndexLiteral {
dest: u8,
container: u8,
literal_idx: u16,
},
/// Multi-level chained indexing (e.g., obj.field1[expr].field2)
ChainedIndex {
/// Index into program's instruction_data.chained_index_params table
params_index: u16,
},
/// Create empty array
ArrayNew {
dest: u8,
},
/// Push element to array
ArrayPush {
arr: u8,
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
params_index: u16,
},
/// Create empty set
SetNew {
dest: u8,
},
/// Add element to set
SetAdd {
set: u8,
value: u8,
},
/// Create set from registers - returns undefined if any element is undefined
SetCreate {
/// Index into program's instruction_data.set_create_params table
params_index: u16,
},
/// Check if collection contains value (for membership testing)
Contains {
dest: u8,
collection: u8,
value: u8,
},
/// Get count/length of collection (arrays, objects, sets) - returns undefined for non-collections
Count {
dest: u8,
collection: u8,
},
/// Assert that two registers are equal - if either is undefined or they differ, fail the condition
AssertEq {
left: u8,
right: u8,
},
/// Consolidated guard instruction — replaces AssertNot, AssertCondition, AssertNotUndefined.
Guard {
register: u8,
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
params_index: u16,
},
/// Continue to next iteration or exit loop
LoopNext {
/// Jump target back to loop body
body_start: u16,
/// Jump target for loop end
loop_end: u16,
},
/// Call rule with caching - checks cache first, executes rule if needed, supports call stack
CallRule {
/// Destination register to store the result of the rule call
dest: u8,
/// Rule index to execute
rule_index: u16,
},
/// Initialize a rule
RuleInit {
/// The register where rule's result is accumulated.
result_reg: u8,
/// The rule number of the rule
rule_index: u16,
},
/// Lookup in data namespace virtual documents (rules + base data)
VirtualDataDocumentLookup {
/// Index into program's instruction_data.virtual_data_document_lookup_params table
params_index: u16,
},
/// Mark successful completion of parameter destructuring validation
DestructuringSuccess {},
/// Return from rule execution
RuleReturn {},
/// Stop execution
Halt {},
/// Begin a comprehension with specified parameters
ComprehensionBegin {
/// Index into program's instruction_data.comprehension_begin_params table
params_index: u16,
},
/// Yield a value to the current comprehension result
ComprehensionYield {
/// Register containing the value to yield to the comprehension
value_reg: u8,
/// Optional register containing the key for object comprehensions
key_reg: Option<u8>,
},
/// End a comprehension block
ComprehensionEnd {},
// ── Azure Policy condition operators (consolidated) ────────────────
/// Consolidated Azure Policy condition instruction.
///
/// Replaces 21 separate Policy* variants. The `op` discriminant selects
/// the specific Azure Policy condition semantics.
///
/// For most ops: `dest = op(left, right)`.
/// For `PolicyOp::Not`: `dest = !is_true(left)`, `right` is unused (0).
/// For `PolicyOp::ValueConditionGuard`: `left` = value register,
/// `right` = condition register.
PolicyCondition {
dest: u8,
left: u8,
right: u8,
op: PolicyOp,
},
// ── AllOf / AnyOf structured short-circuit instructions ───────────
/// Initialize allOf/anyOf: set result register to false.
LogicalBlockStart {
mode: LogicalBlockMode,
/// Register that accumulates the result.
result: u8,
/// PC of the corresponding End instruction.
end_pc: u16,
},
/// Check one allOf child: if not true, short-circuit (result stays false),
/// jump to end_pc.
AllOfNext {
/// Register holding the child condition result.
check: u8,
/// Register that accumulates the allOf result.
result: u8,
/// PC of the AllOfEnd instruction (jump target on short-circuit).
end_pc: u16,
},
/// Check one anyOf child: if true, short-circuit (set result to true),
/// jump to end_pc.
AnyOfNext {
/// Register holding the child condition result.
check: u8,
/// Register that accumulates the anyOf result.
result: u8,
/// PC of the AnyOfEnd instruction.
end_pc: u16,
},
/// Finalize allOf/anyOf block.
///
/// For AllOf: all children passed → set result to true.
/// For AnyOf: no child matched → result stays false (no-op).
LogicalBlockEnd {
mode: LogicalBlockMode,
/// Register that accumulates the result.
result: u8,
},
}
impl Instruction {
/// Create a new LoopStart instruction with parameter table index
pub const fn loop_start(params_index: u16) -> Self {
Self::LoopStart { params_index }
}
/// Create a new BuiltinCall instruction with parameter table index
pub const fn builtin_call(params_index: u16) -> Self {
Self::BuiltinCall { params_index }
}
/// Create a new HostAwait instruction
pub const fn host_await(dest: u8, arg: u8, id: u8) -> Self {
Self::HostAwait { dest, arg, id }
}
/// Create a new FunctionCall instruction with parameter table index
pub const fn function_call(params_index: u16) -> Self {
Self::FunctionCall { params_index }
}
/// Create a new ObjectCreate instruction with parameter table index
pub const fn object_create(params_index: u16) -> Self {
Self::ObjectCreate { params_index }
}
/// Create a new ArrayCreate instruction with parameter table index
pub const fn array_create(params_index: u16) -> Self {
Self::ArrayCreate { params_index }
}
/// Create a new SetCreate instruction with parameter table index
pub const fn set_create(params_index: u16) -> Self {
Self::SetCreate { params_index }
}
/// Create a new ComprehensionBegin instruction with parameter table index
pub const fn comprehension_begin(params_index: u16) -> Self {
Self::ComprehensionBegin { params_index }
}
/// Create a new ComprehensionYield instruction
pub const fn comprehension_yield(value_reg: u8) -> Self {
Self::ComprehensionYield {
value_reg,
key_reg: None,
}
}
/// Create a new ComprehensionYield instruction for object comprehensions
pub const fn comprehension_yield_object(key_reg: u8, value_reg: u8) -> Self {
Self::ComprehensionYield {
value_reg,
key_reg: Some(key_reg),
}
}
/// Create a new ComprehensionEnd instruction
pub const fn comprehension_end() -> Self {
Self::ComprehensionEnd {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::mem::size_of;
#[test]
fn instruction_size() {
// Lock the instruction size to detect unintended growth.
// Without repr(C), Rust picks a 1-byte discriminant (< 256 variants)
// plus 4 bytes for the largest payload variant + 1 byte alignment
// padding for u16 fields = 6 bytes total.
//
// TODO: Reduce to 4 bytes by making LoopNext zero-payload (both fields
// are redundant with LoopStart params) and moving IndexLiteral to a
// params table.
let size = size_of::<Instruction>();
assert_eq!(
size, 6,
"Instruction size changed from 6 to {size} — review new variants for bloat"
);
}
}