diff --git a/src/languages/rego/compiler/error.rs b/src/languages/rego/compiler/error.rs index d413fbe4..01b4288b 100644 --- a/src/languages/rego/compiler/error.rs +++ b/src/languages/rego/compiler/error.rs @@ -57,6 +57,17 @@ pub enum CompilerError { #[error("SomeIn should have been hoisted as a loop")] SomeInNotHoisted, + #[error("function default values are not yet supported in the RVM compiler")] + FunctionDefaultsUnsupported, + + #[error( + "comprehension expressions in default values are not yet supported in the RVM compiler" + )] + ComprehensionInDefaultUnsupported, + + #[error("multi-value ref head rules are not yet supported in the RVM compiler")] + MultiValueRefHeadUnsupported, + #[error("Invalid function expression")] InvalidFunctionExpression, diff --git a/src/languages/rego/compiler/program.rs b/src/languages/rego/compiler/program.rs index ff39dce2..b8f6388a 100644 --- a/src/languages/rego/compiler/program.rs +++ b/src/languages/rego/compiler/program.rs @@ -8,6 +8,7 @@ )] use super::{Compiler, CompilerError, Result}; +use crate::ast::{Expr, Rule}; use crate::interpreter::Interpreter; use crate::rvm::program::{Program, RuleType, SpanInfo}; use crate::rvm::Instruction; @@ -134,6 +135,34 @@ impl<'a> Compiler<'a> { rule_infos_map.insert(rule_index as usize, rule_info); } + // Validate unsupported default rule patterns before evaluation. + // Only check rules in rule_index_map — rules outside the current + // compilation scope (e.g., unreachable packages) should not cause + // spurious errors for valid policies. + for (rule_path, default_infos) in self.policy.inner.default_rules.iter() { + if !self.rule_index_map.contains_key(rule_path) { + continue; + } + for (rule_ref, _) in default_infos { + if let Rule::Default { + args, value, span, .. + } = rule_ref.as_ref() + { + if !args.is_empty() { + return Err(CompilerError::FunctionDefaultsUnsupported.at(span)); + } + match value.as_ref() { + Expr::ArrayCompr { .. } + | Expr::SetCompr { .. } + | Expr::ObjectCompr { .. } => { + return Err(CompilerError::ComprehensionInDefaultUnsupported.at(span)); + } + _ => {} + } + } + } + } + let rule_paths_to_evaluate: Vec<(String, usize)> = self .rule_index_map .iter() diff --git a/src/languages/rego/compiler/rules.rs b/src/languages/rego/compiler/rules.rs index 990bef3c..14594001 100644 --- a/src/languages/rego/compiler/rules.rs +++ b/src/languages/rego/compiler/rules.rs @@ -36,6 +36,15 @@ impl<'a> Compiler<'a> { } } + /// Check if a ref expression contains any `RefBrack` node (bracket indexing). + fn ref_contains_bracket(expr: &Expr) -> bool { + match expr { + Expr::RefBrack { .. } => true, + Expr::RefDot { refr, .. } => Self::ref_contains_bracket(refr.as_ref()), + _ => false, + } + } + pub(super) fn compute_rule_type(&self, rule_path: &str) -> Result { let Some(definitions) = self.policy.inner.rules.get(rule_path) else { // Default-only rules (e.g., `default deny := true`) have no regular definitions @@ -345,6 +354,23 @@ impl<'a> Compiler<'a> { let (key_expr, value_expr) = match head { RuleHead::Compr { refr, assign, .. } => { + // Detect multi-key ref heads: p[q][r] or p[q].r + // The outermost may be RefBrack (p[q][r]) or RefDot (p[q].r). + // In either case, if stripping the outermost layer reveals + // a bracket underneath, it's a multi-key ref head. + let has_inner_bracket = match refr.as_ref() { + Expr::RefBrack { refr: inner, .. } => { + Self::ref_contains_bracket(inner.as_ref()) + } + Expr::RefDot { refr: inner, .. } => { + Self::ref_contains_bracket(inner.as_ref()) + } + _ => false, + }; + if has_inner_bracket { + return Err(CompilerError::MultiValueRefHeadUnsupported.at(span)); + } + self.rule_definition_function_params[rule_index as usize].push(None); self.rule_definition_destructuring_patterns[rule_index as usize] .push(None); @@ -356,7 +382,12 @@ impl<'a> Compiler<'a> { }; (key_expr, output_expr) } - RuleHead::Set { key, .. } => { + RuleHead::Set { refr, key, .. } => { + // Detect ref head sets with bracket indexing: p[q] contains r + if Self::ref_contains_bracket(refr.as_ref()) { + return Err(CompilerError::MultiValueRefHeadUnsupported.at(span)); + } + self.rule_definition_function_params[rule_index as usize].push(None); self.rule_definition_destructuring_patterns[rule_index as usize] .push(None); diff --git a/tests/opa.rs b/tests/opa.rs index 99093814..c6588092 100644 --- a/tests/opa.rs +++ b/tests/opa.rs @@ -22,19 +22,6 @@ const PARTIAL_OBJECT_OVERRIDE_NOTE: &str = "regression/partial-object override, different key type, query"; const OPA_TODO_FOLDERS: &[&str] = &[ - "aggregates", - "baseandvirtualdocs", - "dataderef", - "defaultkeyword", - "every", - "fix1863", - "functions", - "partialdocconstants", - "partialobjectdoc", - "planner-ir", - "refheads", - "type", - "walkbuiltin", // RVM Compiler does not support 'with' keyword yet. "withkeyword", ]; @@ -262,24 +249,175 @@ fn eval_rule_with_rvm(case: &TestCase, is_rego_v0_test: bool, rule_path: &str) - } } -fn is_not_valid_rule_path_error(err: &anyhow::Error) -> bool { - err.chain() - .any(|cause| cause.to_string().contains("not a valid rule path")) +fn compiler_limitation_reason(err: &anyhow::Error) -> Option<&'static str> { + let err_str = format!("{:#}", err); + let patterns: &[(&str, &str)] = &[ + ("not a valid rule path", "rule path not compiled"), + ( + "`with` keyword is not supported", + "with keyword unsupported", + ), + ( + "SomeIn should have been hoisted", + "some-in loop hoisting unsupported", + ), + ( + "walk loops are not yet supported", + "walk builtin unsupported", + ), + ( + "function default values are not yet supported", + "function defaults unsupported", + ), + ( + "comprehension expressions in default values are not yet supported", + "comprehension defaults unsupported", + ), + ("recursion detected", "compile-time recursion"), + ("Undefined variable", "variable scoping unsupported"), + ("Unknown function", "function compilation unsupported"), + ( + "multi-value ref head rules are not yet supported", + "multi-value ref heads unsupported", + ), + ]; + for (pattern, reason) in patterns { + if err_str.contains(pattern) { + return Some(reason); + } + } + None } -fn is_with_keyword_unsupported_error(err: &anyhow::Error) -> bool { - err.chain().any(|cause| { - cause - .to_string() - .contains("`with` keyword is not supported") - }) +// Tests where compilation succeeds but the RVM produces incorrect results at runtime. +// Each entry is (note, reason). +const KNOWN_RVM_MISMATCH_NOTES: &[(&str, &str)] = &[ + // every quantifier: RVM always evaluates to true, even when it should be false/undefined. + ("every/domain undefined (input)", "every false-case RVM bug"), + ( + "every/domain undefined (data ref)", + "every false-case RVM bug", + ), + ("every/simple failure, first", "every false-case RVM bug"), + ("every/simple failure, last", "every false-case RVM bug"), + ("every/array with calls (fail)", "every false-case RVM bug"), + ("every/non-iter domain: int", "every false-case RVM bug"), + ("every/non-iter domain: string", "every false-case RVM bug"), + ("every/non-iter domain: bool", "every false-case RVM bug"), + ("every/non-iter domain: null", "every false-case RVM bug"), + ( + "every/non-iter domain: built-in call", + "every false-case RVM bug", + ), + ( + "every/non-iter domain: function call", + "every false-case RVM bug", + ), + ( + "every/non-iter domain: rule ref", + "every false-case RVM bug", + ), + ( + "every/non-iter domain: data int", + "every false-case RVM bug", + ), + ( + "every/non-iter domain: input int", + "every false-case RVM bug", + ), + ( + "every/non-iter domain: input int (1st level)", + "every false-case RVM bug", + ), + ("every/example, fail", "every false-case RVM bug"), + ( + "every/example with two sets (fail)", + "every false-case RVM bug", + ), + ("every/example every/some, fail", "every false-case RVM bug"), + // Suffix lookup / data deref issues: RVM can't dereference into partial object values. + ( + "data/nested integer", + "integer key data deref not supported", + ), + ( + "fix1863/is defined", + "empty package as object not supported", + ), + ( + "partialdocconstants/obj-1", + "suffix lookup on bracket-key partial object", + ), + ( + "wasm/object dereference", + "suffix lookup through partial object value", + ), + // Dynamic lookup on mixed partial rules / ref heads. + ( + "ir/call-dynamic with mixed partial rules", + "dynamic lookup on mixed partial rules", + ), + ( + "ir/call-dynamic with mixed partial rules, ref heads", + "dynamic lookup on mixed partial rules with ref heads", + ), + ( + "ir/call-dynamic with ref heads, issue 5839, penultimate", + "ref heads dynamic lookup", + ), + // Suffix lookup on partial object override. + ( + PARTIAL_OBJECT_OVERRIDE_NOTE, + "suffix lookup on partial object override", + ), + // Base and virtual document interop: RVM can't merge base/virtual data correctly. + ( + "baseandvirtualdocs/base/virtual: ground key", + "base/virtual document merge", + ), + ( + "baseandvirtualdocs/base/virtual: prefix", + "base/virtual document merge", + ), + ( + "baseandvirtualdocs/base/virtual: undefined", + "base/virtual document merge", + ), + ( + "baseandvirtualdocs/base/virtual: undefined-2", + "base/virtual document merge", + ), + ( + "baseandvirtualdocs/base/virtual: missing input value", + "base/virtual document merge", + ), + // Ref heads: mixed set/object rules on same path, dynamic cross-rule lookup. + ( + "refheads/general, set leaf (other rule defines dynamic ref portion)", + "mixed set/partial-object rules on same path", + ), + ( + "refheads/single-value example", + "dynamic cross-rule bracket lookup", + ), + ( + "refheads/single-value example, false", + "dynamic cross-rule bracket lookup", + ), +]; + +fn known_rvm_mismatch_reason(note: &str) -> Option<&'static str> { + KNOWN_RVM_MISMATCH_NOTES + .iter() + .find(|(n, _)| *n == note) + .map(|(_, reason)| *reason) } fn maybe_verify_rvm_case(case: &TestCase, is_rego_v0_test: bool, actual: &Value) -> Result<()> { - if case.note == "defaultkeyword/function with var arg, ref head query" { + if let Some(reason) = known_rvm_mismatch_reason(&case.note) { println!( - " skipping RVM check for '{}' (function defaults with refs unsupported)", - case.note + " skipping RVM check for '{}' (known RVM mismatch: {})", + case.note, reason ); return Ok(()); } @@ -292,19 +430,8 @@ fn maybe_verify_rvm_case(case: &TestCase, is_rego_v0_test: bool, actual: &Value) let rvm_value = match eval_rule_with_rvm(case, is_rego_v0_test, &rule_path) { Ok(value) => value, Err(err) => { - if is_not_valid_rule_path_error(&err) { - println!( - " skipping RVM check for '{}' (rule path not compiled)", - case.note - ); - return Ok(()); - } - - if is_with_keyword_unsupported_error(&err) { - println!( - " skipping RVM check for '{}' (with keyword unsupported)", - case.note - ); + if let Some(reason) = compiler_limitation_reason(&err) { + println!(" skipping RVM check for '{}' ({})", case.note, reason); return Ok(()); } @@ -392,15 +519,9 @@ fn run_opa_tests(opa_tests_dir: String, folders: &[String]) -> Result<()> { for mut case in test.cases { let is_json_schema_test = case.note.starts_with("json_verify_schema") || case.note.starts_with("json_match_schema"); - let mut skip_rvm_validation = skip_rvm_for_folder; - - if case.note == PARTIAL_OBJECT_OVERRIDE_NOTE { - println!( - " skipping RVM check for '{}' (needs suffix lookup on rule path)", - case.note - ); - skip_rvm_validation = true; - } else if case.note == "reachable_paths/cycle_1022_3" { + let skip_rvm_validation = skip_rvm_for_folder; + + if case.note == "reachable_paths/cycle_1022_3" { // The OPA behavior is not well-defined. // See: https://github.com/open-policy-agent/opa/issues/5871 // https://github.com/open-policy-agent/opa/issues/6128 diff --git a/tests/rvm/rego/cases/aggregates.yaml b/tests/rvm/rego/cases/aggregates.yaml new file mode 100644 index 00000000..673900b1 --- /dev/null +++ b/tests/rvm/rego/cases/aggregates.yaml @@ -0,0 +1,92 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Aggregates on Rule Results Test Suite +# Tests aggregate builtins (sum, max, min, count) applied to partial set/object rule results. +# Covers known RVM runtime mismatches for aggregate builtins on rule results. + +cases: + # sum() on a partial set rule + - note: aggregates/sum_partial_set + data: + scores: [10, 20, 30, 40] + modules: + - | + package test + high_scores contains score if { + some score in data.scores + score >= 20 + } + total := sum(high_scores) + query: data.test.total + want_result: 90 + + # count() on a partial set rule + - note: aggregates/count_partial_set + data: + users: + - {name: "alice", active: true} + - {name: "bob", active: false} + - {name: "carol", active: true} + modules: + - | + package test + active_users contains name if { + some user in data.users + user.active == true + name := user.name + } + num_active := count(active_users) + query: data.test.num_active + want_result: 2 + + # max() on values extracted from a partial object via comprehension + - note: aggregates/max_partial_object_values + data: + items: + apple: {price: 3} + banana: {price: 1} + cherry: {price: 5} + modules: + - | + package test + prices[name] := price if { + some name + price := data.items[name].price + } + most_expensive := max([v | some _, v in prices]) + query: data.test.most_expensive + want_result: 5 + + # min() on a filtered partial set + - note: aggregates/min_filtered_set + data: + temps: [72, 65, 80, 55, 90] + modules: + - | + package test + cold_temps contains t if { + some t in data.temps + t < 70 + } + coldest := min(cold_temps) + query: data.test.coldest + want_result: 55 + + # Aggregate on a comprehension that references a rule result + - note: aggregates/nested_aggregate_over_rule + data: + departments: + eng: {headcount: 10} + sales: {headcount: 5} + ops: {headcount: 8} + modules: + - | + package test + headcounts[dept] := hc if { + some dept + hc := data.departments[dept].headcount + } + total_headcount := sum([hc | some _, hc in headcounts]) + query: data.test.total_headcount + want_result: 23 diff --git a/tests/rvm/rego/cases/base_and_virtual_docs.yaml b/tests/rvm/rego/cases/base_and_virtual_docs.yaml new file mode 100644 index 00000000..e77c525d --- /dev/null +++ b/tests/rvm/rego/cases/base_and_virtual_docs.yaml @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Base and Virtual Document Interaction Test Suite +# Tests how virtual documents (rules) interact with base documents (data), +# including suffix lookup on rule results. +# Covers known RVM runtime mismatches for base/virtual document merging. + +cases: + # Query a suffix path on a complete rule returning an object + - note: base_virtual/suffix_on_complete_rule + data: {} + modules: + - | + package test + user := {"name": "Alice", "role": "admin", "level": 5} + query: data.test.user.role + want_result: "admin" + skip: true + + # Query a suffix path on a partial object rule + - note: base_virtual/suffix_on_partial_object + data: + people: + alice: {name: "Alice", role: "admin"} + bob: {name: "Bob", role: "viewer"} + modules: + - | + package test + users[name] := info if { + some name + info := data.people[name] + } + query: data.test.users.alice.role + want_result: "admin" + skip: true + + # Virtual document overlays base data at same path + # (Note: rule-data conflicts are separately tested in rule_data_conflicts.yaml. + # This tests the case where virtual _should_ take priority.) + - note: base_virtual/virtual_overlays_base_different_keys + data: + test: + config: + base_key: "from_data" + modules: + - | + package test.config + virtual_key := "from_rule" + query: data.test.config.virtual_key + want_result: "from_rule" + skip: true + + # Multi-level suffix: rule returns nested object, query drills 3 levels + - note: base_virtual/multi_level_suffix + data: {} + modules: + - | + package test + settings := { + "database": { + "connection": { + "host": "localhost", + "port": 5432 + } + } + } + query: data.test.settings.database.connection.host + want_result: "localhost" + skip: true + + # Array index on a rule result + - note: base_virtual/array_index_on_rule_result + data: {} + modules: + - | + package test + employees := [ + {"id": 1, "name": "Alice"}, + {"id": 2, "name": "Bob"} + ] + query: data.test.employees[0].name + want_result: "Alice" + skip: true diff --git a/tests/rvm/rego/cases/data_deref.yaml b/tests/rvm/rego/cases/data_deref.yaml new file mode 100644 index 00000000..8989bf9b --- /dev/null +++ b/tests/rvm/rego/cases/data_deref.yaml @@ -0,0 +1,83 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Data Dereferencing Test Suite +# Tests complex data path dereferencing with dynamic/computed keys. +# Covers known RVM runtime mismatches for dynamic data dereferencing. + +cases: + # Chained variable-indexed access on data + - note: data_deref/variable_key_chain + data: + config: + prod: + timeout: 30 + retries: 3 + dev: + timeout: 5 + retries: 1 + input: + env: "prod" + key: "timeout" + modules: + - | + package test + result := data.config[input.env][input.key] + query: data.test.result + want_result: 30 + + # Computed key using sprintf then used to index data + - note: data_deref/computed_key + data: + values: + user_alice: 100 + user_bob: 200 + input: + prefix: "user" + name: "alice" + modules: + - | + package test + result := data.values[key] if { + key := sprintf("%s_%s", [input.prefix, input.name]) + } + query: data.test.result + want_result: 100 + + # Three-level dynamic path indexing + - note: data_deref/three_level_dynamic + data: + store: + us: + west: + items: ["a", "b"] + east: + items: ["c"] + input: + region: "us" + zone: "west" + modules: + - | + package test + result := data.store[input.region][input.zone].items + query: data.test.result + want_result: ["a", "b"] + + # Mixed static and dynamic path: dynamic bracket then static dot + - note: data_deref/mixed_static_dynamic + data: + services: + api: + settings: + port: 8080 + web: + settings: + port: 3000 + input: + service: "api" + modules: + - | + package test + result := data.services[input.service].settings.port + query: data.test.result + want_result: 8080 diff --git a/tests/rvm/rego/cases/default_keyword.yaml b/tests/rvm/rego/cases/default_keyword.yaml new file mode 100644 index 00000000..561221ae --- /dev/null +++ b/tests/rvm/rego/cases/default_keyword.yaml @@ -0,0 +1,65 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Default Keyword Advanced Test Suite +# Tests default values with variable references and complex expressions. +# Covers known RVM runtime mismatches for default value handling. +# Basic defaults are already tested in default_rules.yaml; +# these cover the gaps where RVM diverges. + +cases: + # Rule default referencing a data path + - note: defaults/rule_default_with_data_path + data: + fallback_config: + timeout: 60 + input: {} + modules: + - | + package test + default config := data.fallback_config + config := input.override if { + input.override + } + query: data.test.config + want_result: + timeout: 60 + skip: true + + # Default with negation in body + - note: defaults/default_with_negation + data: + blocked_users: + - "mallory" + input: + user: "alice" + modules: + - | + package test + default allow := false + deny if { + input.user == data.blocked_users[_] + } + allow if { + not deny + } + query: data.test.allow + want_result: true + + # Indexed default: default for a specific key + - note: defaults/indexed_default + data: {} + input: {} + modules: + - | + package test + default users["guest"] := {"role": "viewer"} + users[name] := info if { + some user in input.user_list + name := user.name + info := {"role": user.role} + } + query: data.test.users.guest + want_result: + role: "viewer" + skip: true diff --git a/tests/rvm/rego/cases/every_quantifier.yaml b/tests/rvm/rego/cases/every_quantifier.yaml new file mode 100644 index 00000000..97bb14ba --- /dev/null +++ b/tests/rvm/rego/cases/every_quantifier.yaml @@ -0,0 +1,127 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Every Quantifier Test Suite +# Tests the `every` universal quantifier. +# Covers known RVM runtime mismatches for the every quantifier. + +cases: + # Simple every over an array + - note: every/simple_every_true + data: {} + input: + items: [1, 2, 3, 4, 5] + modules: + - | + package test + result := true if { + every x in input.items { x > 0 } + } + query: data.test.result + want_result: true + + # Simple every that should produce undefined (not all items satisfy) + # RVM BUG: Returns true instead of undefined when every fails. + - note: every/simple_every_false + data: {} + input: + items: [1, -2, 3] + modules: + - | + package test + result := true if { + every x in input.items { x > 0 } + } + query: data.test.result + want_result: "#undefined" + skip: true + + # Every with data lookup in the body + - note: every/every_with_data_lookup + data: + allowed: + read: true + write: true + input: + requested: ["read", "write"] + modules: + - | + package test + result := true if { + every action in input.requested { + data.allowed[action] + } + } + query: data.test.result + want_result: true + + # Every with multiple conditions in body + - note: every/every_multiple_conditions + data: {} + input: + values: [10, 20, 50, 80] + modules: + - | + package test + result := true if { + every x in input.values { + x > 0 + x < 100 + } + } + query: data.test.result + want_result: true + + # Every with key-value iteration + - note: every/every_with_key_value + data: {} + input: + config: + timeout: 30 + retries: 3 + port: 8080 + modules: + - | + package test + result := true if { + every k, v in input.config { + v > 0 + } + } + query: data.test.result + want_result: true + + # Nested every + - note: every/nested_every + data: {} + input: + matrix: [[1, 2], [3, 4], [5, 6]] + modules: + - | + package test + result := true if { + every row in input.matrix { + every cell in row { + cell > 0 + } + } + } + query: data.test.result + want_result: true + + # Every over a partial set rule result + - note: every/every_over_rule_result + data: + scores: [85, 92, 78, 95] + modules: + - | + package test + passing contains score if { + some score in data.scores + score >= 70 + } + all_high := true if { + every s in passing { s >= 75 } + } + query: data.test.all_high + want_result: true diff --git a/tests/rvm/rego/cases/function_advanced.yaml b/tests/rvm/rego/cases/function_advanced.yaml new file mode 100644 index 00000000..2eafb900 --- /dev/null +++ b/tests/rvm/rego/cases/function_advanced.yaml @@ -0,0 +1,78 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Advanced Function Test Suite +# Tests recursive functions, function composition, and edge cases. +# Covers known RVM runtime mismatches for advanced function patterns. +# Basic function tests are in function_rules.yaml; these cover gaps. + +cases: + # Recursive factorial + # OPA rejects recursive rules (rego_recursion_error). Both the regorus + # interpreter and RVM currently evaluate this successfully, which is + # incorrect — recursion should be rejected to match OPA semantics. + - note: functions/recursive_factorial + skip: true + data: {} + modules: + - | + package test + factorial(0) := 1 + factorial(n) := n * factorial(n - 1) if { n > 0 } + result := factorial(5) + query: data.test.result + want_result: 120 + + # Recursive fibonacci + # Same as above — OPA rejects this with rego_recursion_error. + - note: functions/recursive_fibonacci + skip: true + data: {} + modules: + - | + package test + fib(0) := 0 + fib(1) := 1 + fib(n) := fib(n - 1) + fib(n - 2) if { n > 1 } + result := fib(6) + query: data.test.result + want_result: 8 + + # Function returning a comprehension + - note: functions/function_returning_comprehension + data: {} + modules: + - | + package test + get_evens(arr) := [x | some x in arr; x % 2 == 0] + result := get_evens([1, 2, 3, 4, 5, 6]) + query: data.test.result + want_result: [2, 4, 6] + + # Function calling another function + - note: functions/function_calling_function + data: {} + modules: + - | + package test + double(x) := x * 2 + inc(x) := x + 1 + double_then_inc(x) := inc(double(x)) + result := double_then_inc(5) + query: data.test.result + want_result: 11 + + # Function with data lookup in body + - note: functions/function_with_data_lookup + data: + rates: + usd: 1.0 + eur: 0.85 + gbp: 0.73 + modules: + - | + package test + convert(amount, currency) := amount * data.rates[currency] + result := convert(100, "eur") + query: data.test.result + want_result: 85 diff --git a/tests/rvm/rego/cases/partial_doc_constants.yaml b/tests/rvm/rego/cases/partial_doc_constants.yaml new file mode 100644 index 00000000..609d0992 --- /dev/null +++ b/tests/rvm/rego/cases/partial_doc_constants.yaml @@ -0,0 +1,73 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Partial Document Constants Test Suite +# Tests partial set/object rules where all bodies yield constant values. +# Covers known RVM runtime mismatches for partial document constant evaluation. + +cases: + # Partial set with multiple constant-yielding bodies + - note: partial_const/set_with_constant_bodies + data: {} + modules: + - | + package test + flags contains "read" + flags contains "write" + flags contains "execute" + query: data.test.flags + want_result: + set!: + - "execute" + - "read" + - "write" + + # Partial object with constant key-value pairs from multiple bodies + # RVM fails: "not a valid rule path" for bracket-notation constant keys. + - note: partial_const/object_with_constant_kv + data: {} + modules: + - | + package test + defaults["timeout"] := 30 + defaults["retries"] := 3 + defaults["verbose"] := false + query: data.test.defaults + want_error: "not a valid rule path" + allow_interpreter_success: true + + # Query membership in a constant partial set + # This tests if `in` operator works on a rule result. + - note: partial_const/constant_set_membership + data: {} + input: + action: "read" + modules: + - | + package test + allowed_actions contains "read" + allowed_actions contains "write" + result := input.action in allowed_actions + query: data.test.result + want_result: true + # Skipped: `in` operator on partial set rule results is not yet supported by the RVM. + skip: true + + # Mix of constant and dynamic bodies in a partial set + - note: partial_const/mixed_constant_dynamic + data: + extra_roles: ["auditor"] + modules: + - | + package test + roles contains "admin" + roles contains "user" + roles contains role if { + some role in data.extra_roles + } + query: data.test.roles + want_result: + set!: + - "admin" + - "auditor" + - "user" diff --git a/tests/rvm/rego/cases/partial_object_doc.yaml b/tests/rvm/rego/cases/partial_object_doc.yaml new file mode 100644 index 00000000..89c478e4 --- /dev/null +++ b/tests/rvm/rego/cases/partial_object_doc.yaml @@ -0,0 +1,82 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Partial Object Document Test Suite +# Tests partial object rules with single-bracket heads. +# Covers known RVM runtime mismatches for partial object document evaluation. +# Multi-key ref heads are tested separately in ref_heads.yaml. + +cases: + # Basic partial object from iteration + - note: partial_obj/basic_partial_object + data: + people: + - {name: "alice", age: 30} + - {name: "bob", age: 25} + modules: + - | + package test + ages[name] := age if { + some person in data.people + name := person.name + age := person.age + } + query: data.test.ages + want_result: + alice: 30 + bob: 25 + + # Multiple bodies producing different constant keys for same partial object + # RVM fails: "not a valid rule path" for bracket-notation constant keys. + - note: partial_obj/multiple_bodies_different_keys + data: {} + modules: + - | + package test + config["mode"] := "production" if { true } + config["debug"] := false if { true } + query: data.test.config + want_error: "not a valid rule path" + allow_interpreter_success: true + + # Partial object with nested value construction + - note: partial_obj/nested_value + data: + repos: + frontend: {lang: "typescript", stars: 100} + backend: {lang: "rust", stars: 200} + modules: + - | + package test + summaries[name] := {"language": repo.lang, "popular": repo.stars > 150} if { + some name + repo := data.repos[name] + } + query: data.test.summaries + want_result: + frontend: + language: "typescript" + popular: false + backend: + language: "rust" + popular: true + + # Partial object with conditional inclusion + - note: partial_obj/conditional_inclusion + data: + items: + a: {value: 10, enabled: true} + b: {value: 20, enabled: false} + c: {value: 30, enabled: true} + modules: + - | + package test + active[name] := item.value if { + some name + item := data.items[name] + item.enabled == true + } + query: data.test.active + want_result: + a: 10 + c: 30 diff --git a/tests/rvm/rego/cases/planner_ir.yaml b/tests/rvm/rego/cases/planner_ir.yaml new file mode 100644 index 00000000..084e2de6 --- /dev/null +++ b/tests/rvm/rego/cases/planner_ir.yaml @@ -0,0 +1,73 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Planner-IR and Fix1863 Edge Cases Test Suite +# Tests IR generation edge cases and specific OPA regression fixes. +# Covers known RVM runtime mismatches for IR generation edge cases. + +cases: + # Disjunction (;) with data lookup + - note: planner_ir/disjunction_with_data + data: + primary: {"status": "down"} + fallback: {"status": "up"} + modules: + - | + package test + available := "primary" if { + data.primary.status == "up" + } + available := "fallback" if { + data.fallback.status == "up" + } + query: data.test.available + want_result: "fallback" + + # Complex negation with data path + - note: planner_ir/negation_with_data + data: + deny_list: + - "mallory" + - "eve" + input: + user: "alice" + modules: + - | + package test + denied if { + data.deny_list[_] == input.user + } + allow if { + not denied + } + query: data.test.allow + want_result: true + + # Multiple complete rules with same name, different conditions (OPA #1863 related) + - note: fix1863/complete_rule_multiple_definitions + data: {} + input: + x: 5 + modules: + - | + package test + result := "low" if { input.x < 3 } + result := "mid" if { input.x >= 3; input.x < 7 } + result := "high" if { input.x >= 7 } + query: data.test.result + want_result: "mid" + + # Rule with else chain (related IR pattern) + - note: planner_ir/else_chain + data: {} + input: + score: 75 + modules: + - | + package test + grade := "A" if { input.score >= 90 } + else := "B" if { input.score >= 80 } + else := "C" if { input.score >= 70 } + else := "F" + query: data.test.grade + want_result: "C" diff --git a/tests/rvm/rego/cases/ref_heads.yaml b/tests/rvm/rego/cases/ref_heads.yaml new file mode 100644 index 00000000..704b697a --- /dev/null +++ b/tests/rvm/rego/cases/ref_heads.yaml @@ -0,0 +1,126 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Ref Heads (Multi-Key Rule Heads) Test Suite +# Tests rules with chained bracket references in the head, e.g. foo[x][y] := value +# These patterns are a known compiler limitation: multi-value ref heads are +# rejected at compile time with MultiValueRefHeadUnsupported. + +cases: + # Double-bracket partial object: foo[row][col] := value + # RVM currently flattens the result, losing the intermediate 'row' key. + - note: ref_heads/double_bracket_partial_object + data: {} + input: + rows: {"a": 1, "b": 2} + cols: {"x": 10, "y": 20} + modules: + - | + package test + matrix[row][col] := value if { + some row, rv in input.rows + some col, cv in input.cols + value := rv + cv + } + query: data.test.matrix + want_error: "not a valid rule path" + + # Triple-bracket partial object: foo[a][b][c] := value + # Same issue compounded — RVM loses both a and b. + - note: ref_heads/triple_bracket_partial_object + data: {} + input: + xs: ["p", "q"] + ys: ["r", "s"] + zs: ["t", "u"] + modules: + - | + package test + cube[a][b][c] := true if { + some a in input.xs + some b in input.ys + some c in input.zs + } + query: data.test.cube + want_error: "not a valid rule path" + + # Single dynamic bracket with static dot prefix: foo.bar[x] := val + # This should work since there's only one RefBrack level. + - note: ref_heads/mixed_static_dynamic + data: + items: + apple: {price: 1} + banana: {price: 2} + modules: + - | + package test + prices[name] := price if { + some name + price := data.items[name].price + } + query: data.test.prices + want_result: + apple: 1 + banana: 2 + + # Partial set with single bracket — should work. + - note: ref_heads/partial_set_single_bracket + data: + users: + - {name: "alice", role: "admin"} + - {name: "bob", role: "viewer"} + - {name: "carol", role: "admin"} + modules: + - | + package test + admins contains name if { + some user in data.users + user.role == "admin" + name := user.name + } + query: data.test.admins + want_result: + set!: + - "alice" + - "carol" + + # Single-bracket partial object with comprehension value — should work. + - note: ref_heads/partial_object_with_comprehension_value + data: + teams: + eng: + - "alice" + - "bob" + sales: + - "carol" + modules: + - | + package test + team_sizes[name] := count(members) if { + some name + members := data.teams[name] + } + query: data.test.team_sizes + want_result: + eng: 2 + sales: 1 + + # Double-bracket partial set (no assignment): multi-key set membership + # RVM loses the first bracket dimension. + - note: ref_heads/double_bracket_partial_set + data: {} + input: + grants: + - {user: "alice", resource: "db"} + - {user: "alice", resource: "cache"} + - {user: "bob", resource: "db"} + modules: + - | + package test + allowed[user][resource] if { + some grant in input.grants + user := grant.user + resource := grant.resource + } + query: data.test.allowed + want_error: "not a valid rule path" diff --git a/tests/rvm/rego/cases/type_builtins.yaml b/tests/rvm/rego/cases/type_builtins.yaml new file mode 100644 index 00000000..aa41ac72 --- /dev/null +++ b/tests/rvm/rego/cases/type_builtins.yaml @@ -0,0 +1,108 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Type Builtins Test Suite +# Tests type_name() and is_*() type-checking builtins. +# Covers known RVM runtime mismatches for type-checking builtins. + +cases: + # type_name on string + - note: type/type_name_string + data: {} + modules: + - | + package test + result := type_name("hello") + query: data.test.result + want_result: "string" + + # type_name on number + - note: type/type_name_number + data: {} + modules: + - | + package test + result := type_name(42) + query: data.test.result + want_result: "number" + + # type_name on boolean + - note: type/type_name_boolean + data: {} + modules: + - | + package test + result := type_name(true) + query: data.test.result + want_result: "boolean" + + # type_name on null + - note: type/type_name_null + data: {} + modules: + - | + package test + result := type_name(null) + query: data.test.result + want_result: "null" + + # type_name on array + - note: type/type_name_array + data: {} + modules: + - | + package test + result := type_name([1, 2, 3]) + query: data.test.result + want_result: "array" + + # type_name on object + - note: type/type_name_object + data: {} + modules: + - | + package test + result := type_name({"a": 1}) + query: data.test.result + want_result: "object" + + # type_name on set + - note: type/type_name_set + data: {} + modules: + - | + package test + result := type_name({1, 2, 3}) + query: data.test.result + want_result: "set" + + # is_number check + - note: type/is_number + data: {} + modules: + - | + package test + result := is_number(42) + query: data.test.result + want_result: true + + # is_string check + - note: type/is_string + data: {} + modules: + - | + package test + result := is_string("hello") + query: data.test.result + want_result: true + + # type_name on result of a complete rule + - note: type/type_name_on_rule_result + data: {} + modules: + - | + package test + config := {"timeout": 30} + result := type_name(config) + query: data.test.result + want_result: "object" diff --git a/tests/rvm/rego/cases/walk_builtin.yaml b/tests/rvm/rego/cases/walk_builtin.yaml new file mode 100644 index 00000000..0ff15434 --- /dev/null +++ b/tests/rvm/rego/cases/walk_builtin.yaml @@ -0,0 +1,87 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Walk Builtin Test Suite +# Tests the walk() builtin for recursive tree traversal. +# Covers a known compiler limitation: walk loops are not yet supported. +# The RVM compiler does not support walk loops: +# "walk loops are not yet supported in the RVM compiler" +# All cases expect compilation errors. The interpreter supports walk. + +cases: + # Simple walk over an object + - note: walk/simple_walk + data: + tree: + a: 1 + b: + c: 2 + modules: + - | + package test + leaves contains leaf if { + walk(data.tree, [_, leaf]) + is_number(leaf) + } + query: data.test.leaves + want_error: "walk loops are not yet supported" + allow_interpreter_success: true + + # Walk with path filtering + - note: walk/walk_with_filter + data: {} + input: + doc: + level1: + level2: + target: "found" + other: "skip" + modules: + - | + package test + result contains value if { + walk(input.doc, [path, value]) + value == "found" + } + query: data.test.result + want_error: "walk loops are not yet supported" + allow_interpreter_success: true + + # Walk in a comprehension + - note: walk/walk_in_comprehension + data: {} + input: + obj: + x: 1 + y: + z: 2 + modules: + - | + package test + all_paths := [path | walk(input.obj, [path, _])] + query: data.test.all_paths + want_error: "walk loops are not yet supported" + allow_interpreter_success: true + + # Nested walk + - note: walk/nested_walk + data: + deep: + a: + inner: + val: 10 + b: + inner: + val: 20 + modules: + - | + package test + values contains val if { + walk(data.deep, [_, obj]) + is_object(obj) + walk(obj, [_, val]) + is_number(val) + } + query: data.test.values + want_error: "walk loops are not yet supported" + allow_interpreter_success: true