Add comprehensive documentation and GitHub Copilot configuration: - docs/knowledge/: 17 deep-dive knowledge files covering value semantics, RVM architecture, builtins, FFI boundary, feature composition, error handling migration, policy evaluation security, Rego semantics, interpreter/compiler architecture, Azure Policy/RBAC, engine API, time builtins, language extension guide, tooling architecture, causality/partial eval, Rego compiler, Azure Policy aliases, and telemetry/diagnostics - .github/agents/: 16 role-specific AI agent definitions (red-teamer, semantics-expert, architect, performance-engineer, test-engineer, verification-engineer, security-auditor, reliability-engineer, support-engineer, ci-engineer, refactorer, api-steward, program-manager, demo-engineer, dx-engineer, tech-lead) - .github/skills/: 6 workflow skill definitions (thorough-review, design-alternatives, add-builtin, opa-conformance, security-review, verification) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
7.9 KiB
Knowledge: Interpreter Architecture
Deep knowledge about the tree-walking interpreter (src/interpreter.rs).
This is a 4,400+ line file and the legacy execution path. Read this before
modifying evaluation logic.
Core Data Structures
Interpreter State
pub struct Interpreter {
compiled_policy: Rc<CompiledPolicyData>,
data: Value, // Data document (rules materialize here)
input: Value, // User-provided input
with_document: Value, // Temporary overrides via `with`
scopes: Vec<Scope>, // Variable binding stack
contexts: Vec<Context>, // Evaluation context stack
processed: BTreeSet<Ref<Rule>>, // Rules already evaluated
processed_paths: Value, // Data paths already evaluated
rule_values: RuleValues, // Cached rule evaluation results
active_rules: Vec<Ref<Rule>>, // Stack for cycle detection
loop_var_values: ExprLookup, // Loop variable cache
builtins_cache: BTreeMap<..., Value>, // Builtin result cache
execution_timer: ExecutionTimer, // Time limit enforcement
extensions: Map<String, (u8, Rc<Box<dyn Extension>>)>,
with_functions: BTreeMap<String, FunctionModifier>,
}
Context Stack
Each query/rule evaluation pushes a Context:
struct Context {
key_expr: Option<ExprRef>, // Object comprehension key
output_expr: Option<ExprRef>, // Output value expression
value: Value, // Accumulated results
result: Option<QueryResult>, // For user queries (bindings + expressions)
rule_ref: Option<ExprRef>, // Reference to current rule
rule_value: Value, // Computed rule value
is_compr: bool, // Comprehension context
is_set: bool, // Set rule context
is_old_style_set: bool, // Legacy set syntax
early_return: bool, // Break out of evaluation
}
Contexts are pushed for: rule bodies, comprehensions, user queries. The context determines how results are collected (array, set, object, or query result bindings).
Scope Stack
Variables are tracked in a stack of scopes:
type Scope = BTreeMap<SourceStr, Value>;
Each function/rule call pushes a new scope. Variable lookup searches from innermost to outermost scope.
Evaluation Call Hierarchy
eval_rule() Entry: evaluate a named rule
└─ eval_rule_impl() Dispatch by rule type (Spec/Default/Func)
└─ eval_rule_bodies() Evaluate rule body alternatives
└─ eval_query() Execute a query (ordered statements)
└─ eval_stmts() Execute statements in scheduled order
└─ eval_stmt() Single statement dispatch
└─ eval_stmt_impl()
├─ Expr → eval_expr()
├─ SomeIn → eval_some_in()
├─ SomeVars → variable declaration
├─ NotExpr → negation wrapper
└─ Every → eval_every()
eval_expr() Expression dispatcher (25+ variants)
├─ Literals → direct Value
├─ Var/RefDot/RefBrack → eval_chained_ref_dot_or_brack()
├─ BinExpr → eval_bin_expr()
├─ BoolExpr → eval_bool_expr()
├─ ArithExpr → eval_arith_expr()
├─ Call → eval_call()
├─ ArrayCompr/SetCompr/ObjectCompr → eval_*_compr()
├─ Array/Set/Object → eval_array/set/object()
└─ AssignExpr → execute_destructuring_plan()
Rule Evaluation Lifecycle
1. Rule Discovery
When code references data.pkg.rule, the interpreter calls
ensure_rule_evaluated() which:
- Checks if the path has initial data (from
add_data()) - Looks for rules that define that path in
compiled_policy.rules - Evaluates those rules if not already in
self.processed
2. Rule Bodies
A rule can have multiple bodies (alternatives). Bodies are evaluated in order. First successful body wins — remaining bodies are skipped.
allow { condition_a } # Body 1
allow { condition_b } # Body 2 — only tried if body 1 fails
3. Result Collection
Results are collected into ctx.value based on rule type:
- Complete rules: single Value
- Partial set rules:
Value::Setaccumulating members - Partial object rules:
Value::Objectaccumulating key-value pairs
4. Data Materialization
update_rule_value() navigates the rule's path and inserts the result into
self.data. This is how rules become "virtual documents" accessible via
data.pkg.rule.
Precedence: initial data > evaluated rules > default rules.
Variable Lookup
lookup_var() is the main variable resolution function. The search order:
- Local scopes (innermost to outermost)
inputdocument (if name is "input")datadocument (if name is "data") — triggers lazy rule evaluation- Imported variables from other packages
- Returns
Undefinedif not found
Key subtlety: Looking up a data path may trigger rule evaluation, which
may trigger further lookups — this is how lazy evaluation chains work.
The with Modifier
with temporarily overrides data, input, or functions during evaluation:
x = eval { y = f(1) with f as g with data.config as override }
State Save/Restore Pattern
The interpreter saves 7 fields as a tuple before applying with:
(with_document, input, data, processed, processed_paths, with_functions, rule_values)
After applying overrides:
self.processedis cleared (forces re-evaluation with new context)self.rule_valuesis cleared- The expression is evaluated
- All 7 fields are restored
Function overrides:
FunctionModifier::Value(v)— replace function with constantFunctionModifier::Function(path)— replace with another function
Cycle Detection
The interpreter tracks active_rules (a stack of currently-evaluating rules).
If the same rule appears twice in the stack, a cycle is detected and an error
is raised with a "depends on" chain for debugging.
Destructuring Plans
The interpreter executes pre-computed DestructuringPlans for pattern matching
in assignments and some...in bindings:
DestructuringPlan::Var— bind to variableDestructuringPlan::Ignore— wildcard_DestructuringPlan::EqualityValue— match against literalDestructuringPlan::Array— destructure array elementsDestructuringPlan::Object— destructure object fields
Plans are computed at compile time by src/compiler/destructuring_planner/.
Performance-Critical Paths
- Loop variable caching (
loop_var_values): avoids re-evaluating loop expressions on each iteration - Builtin result caching (
builtins_cache): memoizes pure builtin calls - Rule processing tracking (
processed): prevents redundant evaluation - Execution timer: cooperative checking with amortized overhead
Known TODOs in Code
The interpreter has ~15 TODO comments indicating areas of active development:
- Recursive calls with different values for same expression
- Type coercion behavior verification
- With modifier optimization (delay state restore)
- Variable lookup timing questions
- Copy optimization for paths
These indicate areas where the code is known to be evolving. Extra care is needed when modifying near these comments.
Connection to RVM
Both the interpreter and RVM:
- Use the same
BUILTINSregistry - Share the
Valuetype - Use the same
CompiledPolicyData(schedules, hoisted loops) - Produce the same results for the same inputs (semantic equivalence)
When implementing features, they must work in both execution paths.