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>
9.8 KiB
Knowledge: Rego Compiler
Deep knowledge about the Rego → RVM bytecode compiler in
src/languages/rego/compiler/. Read this before modifying rule compilation,
expression codegen, register allocation, or optimization passes.
See also compilation-pipeline.md for the scheduler and loop hoisting stages
that feed into this compiler.
Module Structure
src/languages/rego/compiler/
mod.rs Compiler struct, scope management, register allocation
core.rs Variable resolution, register helpers, instruction emission
program.rs finish() — default rules, rule info construction, metadata
rules.rs Worklist algorithm, per-definition rule compilation
queries.rs Statement compilation, loop hoisting integration
expressions.rs Expression dispatch, recursive compilation
references.rs Chained reference parsing (obj.a[x].b[y])
function_calls.rs Builtin vs. user-defined function dispatch
loops.rs `every` quantifier, loop mode handling
comprehensions.rs Array/Set/Object comprehension compilation
destructuring.rs Function parameter binding/validation
error.rs Error types with span tracking
Worklist Algorithm
Rule compilation uses a worklist (depth-first queue) rather than recursive descent. This provides three benefits:
- Dependency ordering — rules are compiled in reference order
- Recursion detection — a call stack tracks in-progress rules
- Deduplication — already-compiled rules are skipped
while worklist not empty:
pop (rule_path, call_stack) from worklist
if rule_path in call_stack → compile-time recursion error
if rule_path already compiled → skip
push rule_path onto call_stack
compile all definitions of rule_path
mark rule as compiled
When compiling a rule body encounters CallRule to another rule, that
target rule is pushed onto the worklist. This ensures rules are compiled
in call order.
Variable Resolution
The compiler resolves variable names through a priority chain
(core.rs):
1. "input" → emit LoadInput (cached per rule definition)
2. "data" → emit LoadData (cached per rule definition)
3. scope → use bound register from current scope
4. fallback → treat as rule call: data.{package}.{name}
Input/data caching: LoadInput and LoadData are emitted at most
once per rule definition. The cached register is reused for subsequent
references. The cache is reset between definitions to prevent stale state.
Register Allocation
Three-Tier Strategy
Dispatch window — initial registers for entry point dispatch and
temporary work. Sized by dispatch_window_size.
Per-rule window — max registers within any single rule definition.
Register 0 is always the result accumulator. The VM allocates a fixed
frame per rule based on max_rule_window_size.
Per-definition reset — register_counter resets to 0 at each
definition start. This minimizes frame size and enables tail calls.
Special Registers
| Register | Purpose |
|---|---|
| 0 | Rule result accumulator |
current_input_register |
Cached LoadInput (per definition) |
current_data_register |
Cached LoadData (per definition) |
| 0..N-1 (functions) | Function parameter bindings |
Limit: u8 register counter (max 255). The compiler asserts
register_counter < 255.
Expression Compilation
Each Expr variant maps to one or more RVM instructions:
| Expr | Instructions | Notes |
|---|---|---|
| Literal (Num/Str/Bool) | Load |
Literals go to literal table |
true/false/null |
LoadTrue/LoadFalse/LoadNull |
Special-cased |
| Var (in scope) | — | Reuse bound register |
| Var (unresolved) | CallRule |
Treat as rule reference |
| RefDot | IndexLiteral |
Literal key optimization |
| RefBrack | Index or loop |
Depends on bound/unbound index |
| Chained ref | ChainedIndex |
obj.a[x].b[y] → single instruction |
| ArithExpr | Add/Sub/Mul/Div/Mod |
|
| BoolExpr | Eq/Ne/Lt/Le/Gt/Ge |
|
| Not | Not |
|
| Call (builtin) | BuiltinCall |
Via builtin_call_params table |
| Call (user) | FunctionCall |
Via function_call_params table |
| ArrayCompr | ComprehensionBegin..Yield..End |
Mode: Array |
| SetCompr | ComprehensionBegin..Yield..End |
Mode: Set |
| ObjectCompr | ComprehensionBegin..Yield..End |
Mode: Object |
| Every | LoopStart { mode: Every } |
Quantifier loop |
| SomeIn | LoopStart |
Iteration with binding |
| UnaryMinus | Sub (0 - x) |
Chained References
Multi-level property access like input.request.headers["content-type"]
compiles to a single ChainedIndex instruction with parameters:
ChainedIndexParams {
dest: u8,
root: ChainedIndexRoot, // Var or Expr
components: Vec<Component>, // Field(literal_idx) or Expr(register)
}
This avoids emitting multiple Index instructions and intermediate
registers.
Rule Type Compilation
Complete Rules
allow := input.admin == true
- Body compiled as normal statements
- Success:
RuleReturn {}(stores result in register 0) - Static value optimization: if all definitions yield the same constant,
the rule gets
early_exit_on_first_success = true— VM stops after first successful definition
Partial Set Rules
ports contains p if { ... }
- Emit
ComprehensionYield { value_reg, key_reg: None } - Result register accumulates a set of all yielded values
Partial Object Rules
people[name] = age if { ... }
- Emit
ComprehensionYield { value_reg, key_reg: Some(k) } - Result register accumulates key-value pairs
Functions
f(x, y) := x + y
- Parameters bound to registers 0..N-1 before body compilation
DestructuringSuccess {}emitted after parameter validation- Consistent parameter count enforced across all definitions
- After compilation,
FunctionInforecorded with param names
Comprehension Compilation
All comprehensions follow the same pattern:
ComprehensionBegin { mode, collection_reg, body_start, end }
[body: hoisted loops → statements → ComprehensionYield]
ComprehensionEnd {}
Modes: Array, Set, Object. The VM creates the appropriate
collection type and appends each yielded value.
Context stack: the compiler pushes a comprehension context to track that yield should go to the comprehension (not the rule).
Optimization Passes
Constant Folding
try_eval_const() evaluates pure expressions at compile time:
- Array/Set/Object literals with all-constant elements
- Index operations on constant collections
- Result stored in literal table, emitted as
Load
Static Value Detection
After compiling all definitions of a complete rule, the compiler checks if every definition yields the same static value. If so:
early_exit_on_first_success = true- VM stops after first successful definition body
- Common pattern:
default allow := false+allow := true { ... }
Literal Key Optimization
obj["literal"] compiles to IndexLiteral { literal_idx } instead of
loading the string into a register and using Index. Avoids a register
allocation and a Load instruction.
Lazy Builtin Indexing
Builtins are assigned indices only when first used during compilation. The builtin info table contains only actually-referenced builtins, kept in deterministic order (BTreeMap).
Compile-Time Safety
Recursion Detection
The worklist's call stack detects compile-time recursion:
Rule A calls Rule B calls Rule A → error
This prevents infinite compilation loops for mutually recursive rules.
Register Overflow
alloc_register() asserts register_counter < 255. If a rule body
requires more than 255 registers, compilation fails rather than silently
wrapping.
Program Output
The compiler produces Arc<Program> containing:
struct Program {
instructions: Vec<Instruction>, // Bytecode stream
literals: Vec<Value>, // Constant value table
builtin_info_table: Vec<BuiltinInfo>, // Referenced builtins
rule_infos: Vec<RuleInfo>, // Rule metadata
entry_points: IndexMap<String, usize>, // Rule path → instruction offset
instruction_data: InstructionData, // Extended params tables
span_infos: Vec<SpanInfo>, // Source mapping (1:1 with instructions)
}
Every instruction has a corresponding SpanInfo for source mapping,
enabling debugging and IDE integration.
Key Invariants
- Register 0 = result — every rule's result is in register 0
- Input/data cache reset per definition — prevents stale references
- Worklist ordering — rules compiled in call-graph order
- Instruction ↔ SpanInfo 1:1 — every instruction has source location
- Literal table is append-only — indices are stable after emission
Common Pitfalls
-
Scope nesting — comprehensions and
everypush new scopes. Variables bound in inner scopes are not visible in outer scopes. -
Hoisted loop coordination — the compiler must query the hoisting table for each statement to know which loops to emit. Missing a hoisted loop causes incorrect variable binding at runtime.
-
Multi-definition rules — each definition resets registers but shares the same
RuleInfo. Thedefinitionsarray inRuleInforecords instruction ranges for each definition. -
Function parameter count — all definitions of a function must have the same number of parameters. The compiler enforces this.
-
Builtin vs user function — the compiler must distinguish builtin calls (which use
BuiltinCallwith the builtin registry) from user function calls (which useFunctionCallwith the rule index).