microsoft / microsoft/regorus

feat: Support package-level evaluation in RVM

Open
#663 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
346
Forks
75
Avg merge
1d 3h
Merged PRs (30d)
15

Description

Summary

The RVM (Rego Virtual Machine) currently requires individual rule paths as entry points (e.g., data.test.allow). It does not support package-level queries (e.g., data.test) that evaluate all rules in a package and return a merged object of their results.

The interpreter supports this via eval_query("data.test"), but the RVM compiler rejects package paths with "not a valid rule path" since they don't map to any entry in rule_paths.

Motivation

Package-level evaluation is useful for:

  • Evaluating all rules in a package in a single call and receiving a merged result object
  • Parity with the interpreter's eval_query behavior
  • Supporting patterns where rules with defaults coexist (e.g., default allow := false + default deny := true) and the caller wants the full package state
Current Behavior
package test
default allow := false
default deny := true
allow := true if { false }
deny := false if { false }
Query Interpreter RVM
data.test.allow false false
data.test.deny true true
data.test {"allow": false, "deny": true} Error: "not a valid rule path"
Expected Behavior

data.test should return {"allow": false, "deny": true} — evaluating all rules in the test package and merging results into an object.

Technical Context

The RVM compiler resolves entry points in compile_from_policy via get_or_assign_rule_index(), which looks up the path in the rules map (keyed by full rule paths like data.test.allow). A package path like data.test has no entry there, so it fails.

The interpreter handles package queries differently — it evaluates all rules, then walks the data tree to extract the subtree at the requested path.

Why this is non-trivial in the RVM

The RVM compiles each entry point to a CALL_RULE + RETURN sequence that produces a single Value. Package-level evaluation would require:

  1. Discovering all rule paths under a package prefix — e.g., for data.test, find data.test.allow and data.test.deny
  2. Compiling all of them as entry points — the compiler already supports multiple entry points via execute_entry_point_by_index
  3. Merging results into an object — evaluating each rule and assembling {"allow": <result>, "deny": <result>}, handling Undefined (omit from result), nested packages, and partial objects/sets
Possible approaches

Option A — Compiler-level: synthetic package entry point

Add a new entry point type that emits CALL_RULE for each rule in the package, then OBJ_SET instructions to merge results into a single object. This keeps everything in the VM execution loop.

; Synthetic entry point for "data.test"
CALL_RULE  r1 ← rule_0 (allow)
CALL_RULE  r2 ← rule_1 (deny)
OBJ_NEW    r0
OBJ_SET    r0 "allow" r1   ; skip if r1 == Undefined
OBJ_SET    r0 "deny"  r2   ; skip if r2 == Undefined
RETURN     r0

Pros: single VM execution, fast. Cons: more compiler complexity, needs Undefined-skip semantics in OBJ_SET.

Option B — Host-side: iterate entry points and merge

Accept a package path at the API level, resolve it to individual rule paths, compile each as an entry point, execute them via execute_entry_point_by_index, and merge in the host code (Rust or bindings).

// Pseudocode
let rule_paths = compiled_policy.get_rule_paths_for_package("data.test");
let mut result = Value::new_object();
for path in rule_paths {
    let value = vm.execute_entry_point_by_name(&path)?;
    if value != Value::Undefined {
        let key = path.strip_prefix("data.test.").unwrap();
        result.insert(key, value);
    }
}

Pros: simpler, no compiler changes. Cons: multiple VM executions per package, nested packages need recursive handling.

Skipped Tests

The following test cases in tests/rvm/rego/cases/default_rules.yaml are blocked by this limitation:

  • multiple_default_rules_different_names — query data.test with default allow := false + default deny := true
  • default_rule_undefined_vs_default — query data.test with mix of defaulted and undefined rules
Impact

Medium — package-level queries are a common OPA pattern.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/languages/rego/compiler/rules.rs at compile_from_policy and get_or_assign_rule_index(), then compare the interpreter's package-query handling. Review the skipped cases in tests/rvm/rego/cases/default_rules.yaml, especially multiple_default_rules_different_names and default_rule_undefined_vs_default. Done means data.test evaluates all package rules and returns the expected merged object while preserving individual rule-path behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.