Skip to content

feat: Support package-level evaluation in RVM #663

@kusha

Description

@kusha

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions