feat!: Rego -> RVM Compiler and extensive testsuite (#506)

# RVM compiler test cases

Coverage:
- arithmetic
- arrays
- chained lookups
- comparisons
- comprehensions
- default rules
- destructuring
- function rules
- loops/quantifiers
- multiple entrypoints
- objects/sets
- variables
- negative/edge scenarios such as data/rule conflicts
- virtual data lookups
- etc

 # Modify interpreter and compiled policy for RVM Compilation

- Interpreter::eval_default_rule_for_compiler:
   evaluates a named default rule in isolation - allows compiler to emit a constant value instead of instructions
   for the default value

#  feat: Rego Compiler Scaffolding

- Introduce the rego::compiler module surface and entry point wiring
- Add the core compiler concepts:
  - register allocator
  - scope tracking
  - literal/builtin tables
  - rule worklists
  - instruction emit helpers
  - compiler-specific error types
  - context structs for rules, comprehensions, and loops to support later lowering passes.

# feat: Compile Rules/Queries

- add compiler::compile_from_policy workflow plus rule worklist, entry-point wiring, and recursion checks
- implement query lowering:
  - scheduling-aware statement ordering
  - loop hoisting
  - “every/some” semantics
  - context yields
  -  literal assertions
- finalize Program construction

# feat: Expression Lowering

- add compile_rego_expr and helpers to translate every AST expression into RVM instructions,
- interop with binding plans, comprehensions, and membership checks.
- implement collection literal builders (ArrayCreate, SetCreate, ObjectCreate)
  - dedupe literal keys and handle mixed literal/dynamic fields via instruction data blocks.
- operations:
  - arithmetic/boolean/bin operators
  - membership
  - unary minus
  - set unions/intersections
  - etc
- user-defined and builtin function calls
- reference handling
  - analyse chained refs
  - distinguishe data/input/local roots
  - perform rule dispatch or virtual document lookups
  - emits optimized Index/ChainedIndex instructions.

# feat: Comprehensions & Loops

- shared comprehension emitter
 - wraps array/set/object comprehensions with ComprehensionBegin/End
 - context management
- loop lowering utilities
 - read hoisting metadata
 - emit LoopStart/LoopNext
 - some in lowering
 - every quantifiers
 - index iteration
 - propagate binding plans into stored registers so downstream statements see bound variables.

# feat: Destructuring Lowering

- destructuring planner integration
 - assignment/parameter/loop bindings use hoisted plans instead of re-walking ASTs.
- handle :=, =, wildcard matches, and equality
 - evaluate RHS
 - applying destructuring plans
 - emit assert condition as needed
- support nested array/object destructuring, dynamic keys, and some ... in forms

# test: Shared Testing + RVM Suites

- move YAML test helpers into test_utils.rs and re-export via common.rs for use by interpreter and vm test suites
- comprehensive compiler test suite
  - compiles policies with the new Rego→RVM compiler
  - runs them through RegoVM
  - compares against interpreter behavior
  - supports multiple entry points
  - provides assembly listings
  - filterable YAML suites.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2025-11-24 12:08:37 -06:00
committed by GitHub
parent 688e6128d4
commit a3a20a1235
43 changed files with 6945 additions and 139 deletions
+188
View File
@@ -0,0 +1,188 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Shared test utilities for YAML-based test cases
use crate::*;
use anyhow::{bail, Result};
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
/// Process test value specified in json/yaml to interpret special encodings.
pub fn process_value(v: &Value) -> Result<Value> {
match v {
// Handle Undefined encoded as a string "#undefined"
Value::String(s) if s.as_ref() == "#undefined" => Ok(Value::Undefined),
// Handle set encoded as an object
// set! :
// - item1
// - item2
// ...
Value::Object(ref fields) if fields.len() == 1 && matches!(&v["set!"], Value::Array(_)) => {
let mut set_value = Value::new_set();
let set = set_value.as_set_mut()?;
for item in v["set!"].as_array()? {
set.insert(process_value(item)?);
}
Ok(set_value)
}
// Handle complex object specified explicitly:
// object! :
// - key: ...
// value: ...
Value::Object(fields) if fields.len() == 1 && matches!(&v["object!"], Value::Array(_)) => {
let mut object_value = Value::new_object();
let object = object_value.as_object_mut()?;
for item in v["object!"].as_array()? {
object.insert(process_value(&item["key"])?, process_value(&item["value"])?);
}
Ok(object_value)
}
// Recursively process arrays
Value::Array(items) => {
let mut array_value = Value::new_array();
let array = array_value.as_array_mut()?;
for item in items.iter() {
array.push(process_value(item)?);
}
Ok(array_value)
}
// Recursively process objects
Value::Object(fields) => {
let mut object_value = Value::new_object();
let object = object_value.as_object_mut()?;
for (key, value) in fields.iter() {
object.insert(process_value(key)?, process_value(value)?);
}
Ok(object_value)
}
Value::Set(_) => bail!("unexpected set in value read from json/yaml"),
// Simple variants
_ => Ok(v.clone()),
}
}
/// Match computed and expected values with pretty diff output
pub fn match_values(computed: &Value, expected: &Value) -> Result<()> {
if computed != expected {
panic!(
"Values do not match:\nExpected: {:?}\nActual: {:?}",
expected, computed
);
}
Ok(())
}
/// Check output results against expected results
pub fn check_output(computed_results: &[Value], expected_results: &[Value]) -> Result<()> {
if computed_results.len() != expected_results.len() {
bail!(
"the number of computed results ({}) and expected results ({}) is not equal",
computed_results.len(),
expected_results.len()
);
}
for (n, expected_result) in expected_results.iter().enumerate() {
let expected = match process_value(expected_result) {
Ok(e) => e,
_ => bail!("unable to process value :\n {expected_result:?}"),
};
if let Some(computed_result) = computed_results.get(n) {
match match_values(computed_result, &expected) {
Ok(()) => (),
Err(e) => bail!("{e}"),
}
}
}
Ok(())
}
/// Support for single value or multiple values in test input/output
#[derive(PartialEq, Debug, Clone)]
pub enum ValueOrVec {
Single(Value),
Many(Vec<Value>),
}
impl Serialize for ValueOrVec {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
ValueOrVec::Single(value) => value.serialize(serializer),
ValueOrVec::Many(v) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("many!", v)?;
map.end()
}
}
}
}
impl<'de> Deserialize<'de> for ValueOrVec {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
match &value["many!"] {
Value::Array(arr) => Ok(ValueOrVec::Many(arr.to_vec())),
_ => Ok(ValueOrVec::Single(value)),
}
}
}
/// Standard test case structure for YAML tests
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct TestCase {
pub data: Option<Value>,
pub input: Option<ValueOrVec>,
pub modules: Vec<String>,
pub note: String,
pub query: String,
pub entry_points: Option<Vec<String>>,
pub sort_bindings: Option<bool>,
pub want_result: Option<ValueOrVec>,
pub want_results: Option<Vec<ValueOrVec>>,
pub want_prints: Option<Vec<String>>,
pub no_result: Option<bool>,
pub skip: Option<bool>,
pub error: Option<String>,
pub traces: Option<bool>,
pub want_error: Option<String>,
pub want_error_code: Option<String>,
#[serde(default = "default_strict")]
pub strict: bool,
/// Allow interpreter to succeed when RVM fails with conflict detection
pub allow_interpreter_success: Option<bool>,
/// Allow interpreter to produce incorrect results when RVM produces correct results
pub allow_interpreter_incorrect_behavior: Option<bool>,
}
fn default_strict() -> bool {
true
}
/// Standard YAML test file structure
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct YamlTest {
pub cases: Vec<TestCase>,
}
/// Convert ValueOrVec to a vector of Values
pub fn value_or_vec_to_vec(value_or_vec: ValueOrVec) -> Vec<Value> {
match value_or_vec {
ValueOrVec::Single(single_result) => vec![single_result],
ValueOrVec::Many(many_result) => many_result,
}
}
+2 -137
View File
@@ -3,10 +3,11 @@
use std::env;
use crate::test_utils::{check_output, ValueOrVec};
use crate::*;
use anyhow::{bail, Result};
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
use serde::{Deserialize, Serialize};
use test_generator::test_resources;
#[cfg(feature = "azure_policy")]
@@ -122,106 +123,6 @@ mod load_target_definitions {
}
}
// Process test value specified in json/yaml to interpret special encodings.
pub fn process_value(v: &Value) -> Result<Value> {
match v {
// Handle Undefined encoded as a string "#undefined"
Value::String(s) if s.as_ref() == "#undefined" => Ok(Value::Undefined),
// Handle set encoded as an object
// set! :
// - item1
// - item2
// ...
Value::Object(ref fields) if fields.len() == 1 && matches!(&v["set!"], Value::Array(_)) => {
let mut set_value = Value::new_set();
let set = set_value.as_set_mut()?;
for item in v["set!"].as_array()? {
set.insert(process_value(item)?);
}
Ok(set_value)
}
// Handle complex object specified explicitly:
// object! :
// - key: ...
// value: ...
Value::Object(fields) if fields.len() == 1 && matches!(&v["object!"], Value::Array(_)) => {
let mut object_value = Value::new_object();
let object = object_value.as_object_mut()?;
for item in v["object!"].as_array()? {
object.insert(process_value(&item["key"])?, process_value(&item["value"])?);
}
Ok(object_value)
}
// Recursively process arrays
Value::Array(items) => {
let mut array_value = Value::new_array();
let array = array_value.as_array_mut()?;
for item in items.iter() {
array.push(process_value(item)?);
}
Ok(array_value)
}
// Recursively process objects
Value::Object(fields) => {
let mut object_value = Value::new_object();
let object = object_value.as_object_mut()?;
for (key, value) in fields.iter() {
object.insert(process_value(key)?, process_value(value)?);
}
Ok(object_value)
}
Value::Set(_) => bail!("unexpected set in value read from json/yaml"),
// Simple variants
_ => Ok(v.clone()),
}
}
fn match_values(computed: &Value, expected: &Value) -> Result<()> {
if computed != expected {
let expected_yaml = serde_yaml::to_string(&expected)?;
let computed_yaml = serde_yaml::to_string(&computed)?;
panic!(
"expected:\n{}computed:\n{}diff:\n{}",
expected_yaml,
computed_yaml,
prettydiff::diff_chars(&expected_yaml, &computed_yaml)
);
}
Ok(())
}
pub fn check_output(computed_results: &[Value], expected_results: &[Value]) -> Result<()> {
if computed_results.len() != expected_results.len() {
bail!(
"the number of computed results ({}) and expected results ({}) is not equal",
computed_results.len(),
expected_results.len()
);
}
for (n, expected_result) in expected_results.iter().enumerate() {
let expected = match process_value(expected_result) {
Ok(e) => e,
_ => bail!("unable to process value :\n {expected_result:?}"),
};
if let Some(computed_result) = computed_results.get(n) {
match match_values(computed_result, &expected) {
Ok(()) => (),
Err(e) => bail!("{e}"),
}
}
}
Ok(())
}
fn push_query_results(query_results: QueryResults, results: &mut Vec<Value>) {
if query_results.result.len() == 1 {
if let Some(query_result) = query_results.result.last() {
@@ -385,42 +286,6 @@ pub fn eval_file_with_rule_evaluation(
Ok((results, engine.take_prints()?))
}
#[derive(PartialEq, Debug)]
pub enum ValueOrVec {
Single(Value),
Many(Vec<Value>),
}
impl Serialize for ValueOrVec {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
ValueOrVec::Single(value) => value.serialize(serializer),
ValueOrVec::Many(v) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("many!", v)?;
map.end()
}
}
}
}
impl<'de> Deserialize<'de> for ValueOrVec {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
match &value["many!"] {
Value::Array(arr) => Ok(ValueOrVec::Many(arr.to_vec())),
_ => Ok(ValueOrVec::Single(value)),
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct TestCase {
data: Option<Value>,