Files
regorus/tests/rvm/rego/cases/virtual_data_document_lookup.yaml
Anand Krishnamoorthi a3a20a1235 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>
2025-11-24 12:08:37 -06:00

396 lines
13 KiB
YAML

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# VirtualDataDocumentLookup Test Suite
# Tests the four cases of virtual data document lookup:
# 1. All components consumed and rule index found -> evaluate rule
# 2. Rule index found with remaining components -> evaluate rule then index result
# 3. All components consumed but undefined -> apply components to data directly
# 4. Subobject found -> panic (not yet implemented)
cases:
# Case 1: All components consumed and rule index found
- note: rule_index_all_components_consumed
data:
users:
alice: {"name": "Alice", "age": 30}
input:
rule_name: "alice_profile"
modules:
- |
package test.users
alice_profile := data.users.alice
- |
package test
main := result if {
result := data.test.users[input.rule_name]
}
query: data.test.main
want_result: {"name": "Alice", "age": 30}
allow_interpreter_incorrect_behavior: true
# Case 2: Rule index found with remaining components
- note: rule_index_with_remaining_components
data:
users:
alice: {"name": "Alice", "age": 30, "profile": {"bio": "Software Engineer"}}
input:
rule_name: "alice_data"
field1: "profile"
field2: "bio"
modules:
- |
package test.users
alice_data := data.users.alice
- |
package test
main := result if {
result := data.test.users[input.rule_name][input.field1][input.field2]
}
query: data.test.main
want_result: "Software Engineer"
allow_interpreter_incorrect_behavior: true
# Case 2b: Rule index with multiple remaining components
- note: rule_index_multiple_remaining_components
data:
config:
app:
settings: {"theme": "dark", "lang": "en"}
input:
rule: "app_config"
path1: "settings"
path2: "theme"
modules:
- |
package test.config
app_config := data.config.app
- |
package test
main := result if {
result := data.test.config[input.rule][input.path1][input.path2]
}
query: data.test.main
want_result: "dark"
allow_interpreter_incorrect_behavior: true
# Case 3: All components consumed but no rule exists (direct data access)
- note: direct_data_access_no_rules
data:
users:
bob: {"name": "Bob", "age": 25}
input:
person: "bob"
attribute: "name"
modules:
- |
package test
main := result if {
# No rule defined for data.users.bob, should access data directly
result := data.users[input.person][input.attribute]
}
query: data.test.main
want_result: "Bob"
# Case 3b: Direct nested data access (no rules)
- note: direct_nested_data_access
data:
system:
metrics:
cpu: 85
memory: 70
input:
metric: "cpu"
modules:
- |
package test
main := result if {
# No rules defined for data.system.metrics, access data directly
result := data.system.metrics[input.metric]
}
query: data.test.main
want_result: 85
# Case 3c: Direct array indexing (no rules)
- note: direct_array_indexing
data:
inventory:
fruits: ["apple", "banana", "cherry"]
input:
collection: "fruits"
index: 1
modules:
- |
package test
main := result if {
# No rules defined for data.inventory.fruits, access array directly
result := data.inventory[input.collection][input.index]
}
query: data.test.main
want_result: "banana"
# Mixed case: Rule exists at intermediate level, then data access
- note: rule_at_intermediate_level
data:
company:
employees:
- {"name": "Alice", "dept": "Engineering"}
- {"name": "Bob", "dept": "Marketing"}
input:
rule_name: "staff"
idx: 0
field: "name"
modules:
- |
package test.company
staff := data.company.employees
- |
package test
main := result if {
# Rule exists at data.test.company.staff, then access array element
result := data.test.company[input.rule_name][input.idx][input.field]
}
query: data.test.main
want_result: "Alice"
allow_interpreter_incorrect_behavior: true
# Case with dynamic indexing via register
- note: rule_with_dynamic_indexing
data:
products:
electronics: {"laptop": 1200, "phone": 800}
clothing: {"shirt": 25, "pants": 50}
input:
rule: "electronics_catalog"
item: "laptop"
modules:
- |
package test.products
electronics_catalog := data.products.electronics
- |
package test
main := result if {
result := data.test.products[input.rule][input.item]
}
query: data.test.main
want_result: 1200
allow_interpreter_incorrect_behavior: true
# Case 3d: Undefined path access returns undefined
- note: undefined_path_access
data: {}
input:
path2: "path"
path3: "value"
modules:
- |
package test
main := result if {
# This should access undefined data, returning undefined
result := data.nonexistent[input.path2][input.path3]
}
query: data.test.main
want_result: "#undefined"
# Case that actually triggers VirtualDataDocumentLookup:
# Path is a prefix of multiple rules
- note: virtual_lookup_with_rule_prefix
data:
config:
app: {"name": "MyApp", "version": "1.0"}
input:
submodule: "app"
field: "name"
modules:
- |
package test.config.app
name := data.config.app.name
version := data.config.app.version
full_info := {"name": data.config.app.name, "version": data.config.app.version}
- |
package test
main := result if {
# This should trigger VirtualDataDocumentLookup since data.test.config.app
# is a prefix of multiple rules: data.test.config.app.name, data.test.config.app.version, etc.
result := data.test.config[input.submodule][input.field]
}
query: data.test.main
want_result: "MyApp"
# Case 4: Subobject case - evaluate all rules in a subobject and merge with data
- note: subobject_case_multiple_rules
data:
users:
alice: {"name": "Alice", "age": 30}
bob: {"name": "Bob", "age": 25}
permissions:
alice: {"admin": true}
bob: {"admin": false}
modules:
- |
package test.users.alice
profile := {"name": data.users.alice.name, "age": data.users.alice.age}
is_admin := data.permissions.alice.admin
- |
package test.users.bob
profile := {"name": data.users.bob.name, "age": data.users.bob.age}
is_admin := data.permissions.bob.admin
- |
package test
main := result if {
# This should trigger Case 4: all components consumed and we have a subobject
# data.test.users should contain the evaluated rules from both alice and bob packages
result := data.test.users
}
query: data.test.main
want_result:
alice:
profile: {"name": "Alice", "age": 30}
is_admin: true
bob:
profile: {"name": "Bob", "age": 25}
is_admin: false
# Case 4b: Nested subobject evaluation with cache hits
- note: nested_subobject_with_cache_hits
data:
company:
departments:
engineering: {"budget": 1000000}
marketing: {"budget": 500000}
employees:
alice: {"dept": "engineering", "salary": 100000}
bob: {"dept": "marketing", "salary": 70000}
charlie: {"dept": "engineering", "salary": 90000}
modules:
- |
package test.company.departments.engineering
total_budget := data.company.departments.engineering.budget
employee_count := count([e | e := data.company.employees[_]; e.dept == "engineering"])
avg_budget_per_employee := total_budget / employee_count
- |
package test.company.departments.marketing
total_budget := data.company.departments.marketing.budget
employee_count := count([e | e := data.company.employees[_]; e.dept == "marketing"])
avg_budget_per_employee := total_budget / employee_count
- |
package test.company.employees.alice
profile := data.company.employees.alice
department_info := data.test.company.departments[profile.dept] # Should hit cache
- |
package test.company.employees.bob
profile := data.company.employees.bob
department_info := data.test.company.departments[profile.dept] # Should hit cache
- |
package test.company.employees.charlie
profile := data.company.employees.charlie
department_info := data.test.company.departments[profile.dept] # Should hit cache again
- |
package test
main := result if {
# This creates nested subobject evaluations:
# 1. data.test.company (subobject with departments and employees)
# 2. data.test.company.departments (subobject with engineering and marketing)
# 3. data.test.company.employees (subobject with alice, bob, charlie)
# The departments should be cached and reused multiple times
result := {
"company_overview": data.test.company,
"departments_only": data.test.company.departments, # Cache hit for departments
"employees_only": data.test.company.employees, # Cache hit for employees
"engineering_dept": data.test.company.departments.engineering # Cache hit for specific dept
}
}
query: data.test.main
want_result:
company_overview:
departments:
engineering:
total_budget: 1000000
employee_count: 2
avg_budget_per_employee: 500000
marketing:
total_budget: 500000
employee_count: 1
avg_budget_per_employee: 500000
employees:
alice:
profile: {"dept": "engineering", "salary": 100000}
department_info:
total_budget: 1000000
employee_count: 2
avg_budget_per_employee: 500000
bob:
profile: {"dept": "marketing", "salary": 70000}
department_info:
total_budget: 500000
employee_count: 1
avg_budget_per_employee: 500000
charlie:
profile: {"dept": "engineering", "salary": 90000}
department_info:
total_budget: 1000000
employee_count: 2
avg_budget_per_employee: 500000
departments_only:
engineering:
total_budget: 1000000
employee_count: 2
avg_budget_per_employee: 500000
marketing:
total_budget: 500000
employee_count: 1
avg_budget_per_employee: 500000
employees_only:
alice:
profile: {"dept": "engineering", "salary": 100000}
department_info:
total_budget: 1000000
employee_count: 2
avg_budget_per_employee: 500000
bob:
profile: {"dept": "marketing", "salary": 70000}
department_info:
total_budget: 500000
employee_count: 1
avg_budget_per_employee: 500000
charlie:
profile: {"dept": "engineering", "salary": 90000}
department_info:
total_budget: 1000000
employee_count: 2
avg_budget_per_employee: 500000
engineering_dept:
total_budget: 1000000
employee_count: 2
avg_budget_per_employee: 500000
# Test that function rules are excluded from virtual data document lookup
- note: function_rules_excluded_from_virtual_lookup
data:
config:
app_name: "TestApp"
version: "1.0.0"
modules:
- |
package test.config
# Regular rule - should be accessible via virtual lookup
application_info := {"name": data.config.app_name, "version": data.config.version}
# Function rule - should NOT be accessible via virtual lookup
format_version(major, minor) := sprintf("%d.%d", [major, minor])
# Another regular rule - should be accessible
app_status := "running"
- |
package test
main := result if {
# This should only include regular rules, not function rules
# data.test.config should contain: application_info, app_status
# but NOT: format_version (because it's a function rule)
result := data.test.config
}
query: data.test.main
want_result:
application_info: {"name": "TestApp", "version": "1.0.0"}
app_status: "running"
# Note: format_version should NOT appear here since it's a function rule