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>
5.7 KiB
Knowledge: Builtin System
Deep knowledge about regorus's builtin function infrastructure. Read this before adding, modifying, or debugging builtin functions.
Registration Pattern
Builtin functions live in src/builtins/. Each module exports a register
function that inserts entries into the BUILTINS lazy_static registry:
// In src/builtins/arrays.rs
pub fn register(m: &mut BuiltinsMap<&'static str, BuiltinFcn>) {
m.insert("array.concat", (concat, 2));
m.insert("array.reverse", (reverse, 1));
m.insert("array.slice", (slice, 3));
}
The tuple is (function_pointer, arity). The function signature is:
fn concat(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value>
Parameters:
span: Source location for error messagesparams: AST expressions (for error reporting, not evaluation)args: Evaluated argument valuesstrict: Whether strict builtin error mode is enabled
Registration in BUILTINS
All builtin modules register in src/builtins/mod.rs via a lazy_static! block:
lazy_static::lazy_static! {
pub static ref BUILTINS: BuiltinsMap<&'static str, BuiltinFcn> = {
let mut m = BuiltinsMap::new();
numbers::register(&mut m);
strings::register(&mut m);
// ...
#[cfg(feature = "regex")]
regex::register(&mut m);
// ...
m
};
}
Feature Gating
Optional builtins must be feature-gated at two levels:
1. Cargo.toml — declare the feature and optional dependency:
[features]
regex = ["dep:regex"]
2. Registration — gate the register call:
#[cfg(feature = "regex")]
regex::register(&mut m);
3. Composite features — add to full-opa and/or opa-no-std if the
builtin is part of the OPA specification:
full-opa = ["regex", ...]
opa-no-std = ["regex", ...] # only if the dep supports no_std
Argument Validation
Every builtin must validate argument count first:
fn concat(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "array.concat";
ensure_args_count(span, name, params, args, 2)?;
// ...
}
Then validate argument types. Use ensure_* helpers where available.
OPA Conformance Requirements
Error messages must match OPA exactly. The OPA conformance test suite
(tests/opa.rs) compares error messages literally. This means:
- Function names in errors must match OPA's naming
- Error message format must match OPA's format
- Type error descriptions must match OPA's wording
If an error message doesn't match, the conformance test fails. When implementing a builtin, compare against the OPA Go source for exact wording.
Strict vs Non-Strict Mode
When strict is true:
- Type errors are hard errors (return
Err(...)) - Missing arguments are hard errors
When strict is false:
- Type errors return
Value::Undefined(the OPA default) - This matches OPA's behavior where type mismatches silently fail
Undefined Argument Handling
Builtins receive Value::Undefined when an argument expression evaluates to
undefined. The interpreter checks this before calling:
if args.iter().any(|a| a == &Value::Undefined) {
return Ok(Value::Undefined);
}
However, individual builtins may also need to handle Undefined for specific semantic reasons.
Both Execution Paths
Builtins are shared between the interpreter and the RVM. Both use the same
BUILTINS registry. When adding a builtin:
- The interpreter calls builtins via
eval_builtin_call() - The RVM resolves builtins by name from the same registry
- No special RVM registration is needed — it's automatic
Test with both cargo test (interpreter) and RVM-specific tests.
Adding a New Builtin: Checklist
- Create the function in the appropriate
src/builtins/module - Follow the
(span, params, args, strict) -> Result<Value>signature - Call
ensure_args_count()first - Feature-gate if it requires optional dependencies
- Register in the module's
register()function - Add the module's
register()call insrc/builtins/mod.rs(feature-gated) - Add to composite features (
full-opa,opa-no-std) if OPA-standard - Write tests (YAML format, see
tests/interpreter/) - Verify error messages match OPA exactly
- Update
docs/builtins.md - Run
cargo test --test opato verify OPA conformance - Run
cargo xtask ci-debugfor full suite
Builtin Modules
The ~19 modules in src/builtins/ cover:
numbers— arithmetic, rounding, abs, remstrings— concat, contains, replace, split, trim, format, sprintfarrays— concat, reverse, sliceobjects— get, keys, remove, union, filtersets— intersection, union, differenceaggregates— count, sum, min, max, sorttypes— type_name, is_number, is_string, etc.encoding— base64, base64url, hex, json, yaml, urlqueryregex— match, split, find (feature-gated)glob— match (feature-gated)time— now_ns, parse_ns, date, clock (feature-gated)crypto— hashing functionsgraphs— walk, reachable (feature-gated)semver— is_valid, compare (feature-gated)uuid— rfc4122 (feature-gated)net— cidr_contains, cidr_intersects (feature-gated)opa— runtime info (feature-gated)
LRU Caching
Some builtins use the LRU cache (src/cache.rs) for expensive compiled objects:
- Regex patterns: up to 256 cached compiled
regex::Regexobjects - Glob matchers: up to 128 cached compiled
GlobMatcherobjects
The cache is global, thread-safe (mutex-protected), and configurable via
cache::configure(). The hard cap is 2^16 entries per cache type.