mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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>
195 lines
5.7 KiB
Markdown
195 lines
5.7 KiB
Markdown
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
|
|
<!-- Licensed under the MIT License. -->
|
|
|
|
# Knowledge: Error Handling Migration
|
|
|
|
Deep knowledge about regorus's error handling patterns and the ongoing
|
|
migration from `anyhow` to `thiserror`. Read this before adding error
|
|
handling to new code or modifying existing error paths.
|
|
|
|
## Current State
|
|
|
|
The codebase has two error handling approaches coexisting:
|
|
|
|
### Legacy: anyhow (widespread)
|
|
|
|
Most of the codebase uses `anyhow::Result` with `bail!()` and `anyhow!()`:
|
|
|
|
```rust
|
|
use anyhow::{anyhow, bail, Result};
|
|
|
|
fn eval_something(&mut self) -> Result<Value> {
|
|
let v = map.get("key").ok_or_else(|| anyhow!("missing key"))?;
|
|
if condition_fails {
|
|
bail!("evaluation failed: {reason}");
|
|
}
|
|
Ok(value)
|
|
}
|
|
```
|
|
|
|
Found in: `src/interpreter.rs`, `src/engine.rs`, `src/parser.rs`,
|
|
`src/lexer.rs`, `src/value.rs`, `src/number.rs`, `src/builtins/`, and most
|
|
other modules.
|
|
|
|
### Target: thiserror (RVM leads)
|
|
|
|
The RVM uses strongly typed error enums:
|
|
|
|
```rust
|
|
use thiserror::Error;
|
|
|
|
#[derive(Error, Debug, Clone, PartialEq)]
|
|
pub enum VmError {
|
|
#[error("Execution stopped: exceeded maximum instruction limit of {limit} after {executed} instructions (pc={pc})")]
|
|
InstructionLimitExceeded { limit: usize, executed: usize, pc: usize },
|
|
|
|
#[error("Register index {index} out of bounds (pc={pc}, register_count={register_count})")]
|
|
RegisterIndexOutOfBounds { index: u8, pc: usize, register_count: usize },
|
|
|
|
// ... 30+ variants covering every VM error case
|
|
}
|
|
|
|
pub type Result<T> = core::result::Result<T, VmError>;
|
|
```
|
|
|
|
Found in: `src/rvm/vm/errors.rs`
|
|
|
|
## The VmError Pattern (Reference Implementation)
|
|
|
|
Key design principles visible in `VmError`:
|
|
|
|
**1. Every variant carries context:**
|
|
```rust
|
|
InstructionLimitExceeded { limit: usize, executed: usize, pc: usize }
|
|
```
|
|
Not just "limit exceeded" — includes the limit, actual count, and program counter.
|
|
|
|
**2. Program counter in every variant:**
|
|
```rust
|
|
// Every single variant includes `pc: usize`
|
|
RegisterNotObject { register: u8, value: Value, pc: usize },
|
|
LiteralIndexOutOfBounds { index: u16, pc: usize },
|
|
```
|
|
This is a debugging aid — every error can be traced to the exact instruction.
|
|
|
|
**3. Exhaustive coverage:**
|
|
30+ variants covering every known error case. No catch-all "Other(String)".
|
|
|
|
**4. Derives Clone and PartialEq:**
|
|
```rust
|
|
#[derive(Error, Debug, Clone, PartialEq)]
|
|
```
|
|
Clone enables error propagation without ownership transfer. PartialEq enables
|
|
testing error conditions precisely.
|
|
|
|
**5. Type alias for ergonomics:**
|
|
```rust
|
|
pub type Result<T> = core::result::Result<T, VmError>;
|
|
```
|
|
|
|
**6. Bridge from anyhow:**
|
|
```rust
|
|
impl From<anyhow::Error> for VmError {
|
|
fn from(err: anyhow::Error) -> Self {
|
|
VmError::ArithmeticError { message: format!("{}", err), pc: 0 }
|
|
}
|
|
}
|
|
```
|
|
This allows the RVM to call into legacy code that returns `anyhow::Result`.
|
|
|
|
## Migration Strategy
|
|
|
|
### For New Code
|
|
|
|
**Always use thiserror.** Define a module-specific error enum:
|
|
|
|
```rust
|
|
use thiserror::Error;
|
|
|
|
#[derive(Error, Debug, Clone, PartialEq)]
|
|
pub enum MySubsystemError {
|
|
#[error("invalid input: {0}")]
|
|
InvalidInput(String),
|
|
|
|
#[error("resource limit exceeded: {current} > {limit}")]
|
|
ResourceLimitExceeded { current: usize, limit: usize },
|
|
}
|
|
|
|
pub type Result<T> = core::result::Result<T, MySubsystemError>;
|
|
```
|
|
|
|
### For Existing Code
|
|
|
|
When modifying existing functions that use `anyhow`:
|
|
- **Within the same module**: continue with `anyhow` for consistency
|
|
- **At module boundaries**: consider wrapping `anyhow::Error` in a typed variant
|
|
- **Incremental migration**: converting a whole module at once is better than
|
|
mixing styles within a single module
|
|
|
|
### Bridge Pattern
|
|
|
|
When typed-error code calls anyhow code (or vice versa):
|
|
|
|
```rust
|
|
// Typed → anyhow (automatic via anyhow's From impl)
|
|
fn caller() -> anyhow::Result<Value> {
|
|
typed_function()?; // VmError auto-converts to anyhow::Error
|
|
Ok(value)
|
|
}
|
|
|
|
// Anyhow → typed (explicit conversion needed)
|
|
fn caller() -> Result<Value, VmError> {
|
|
anyhow_function().map_err(|e| VmError::Internal {
|
|
message: format!("{}", e),
|
|
pc: current_pc,
|
|
})?;
|
|
Ok(value)
|
|
}
|
|
```
|
|
|
|
## Error Message Guidelines
|
|
|
|
### For OPA Conformance
|
|
|
|
Builtin error messages **must match OPA exactly** — the conformance test suite
|
|
compares literally. When implementing builtins, check the OPA Go source.
|
|
|
|
### For Internal Errors
|
|
|
|
- Include enough context to diagnose without a debugger
|
|
- Include identifiers (register index, PC, rule name, etc.)
|
|
- Don't include sensitive data (user input, policy content)
|
|
- Use structured fields, not string formatting:
|
|
|
|
```rust
|
|
// ✗ Bad
|
|
#[error("register {0} out of bounds at pc {1}")]
|
|
RegisterOutOfBounds(u8, usize),
|
|
|
|
// ✓ Good — named fields are self-documenting
|
|
#[error("register index {index} out of bounds (pc={pc}, register_count={register_count})")]
|
|
RegisterIndexOutOfBounds { index: u8, pc: usize, register_count: usize },
|
|
```
|
|
|
|
## Panic Safety Connection
|
|
|
|
Error handling is the front line of panic safety. The deny lints forbid
|
|
`unwrap()`, `expect()`, `panic!()`, etc. Every fallible operation must return
|
|
`Result`. This is not just style — in daemon mode, a panic crashes the service.
|
|
|
|
The error migration makes this stronger: with typed errors, every failure mode
|
|
is enumerated and the compiler ensures all are handled. With `anyhow`, errors
|
|
are opaque and may be accidentally swallowed.
|
|
|
|
## no_std Compatibility
|
|
|
|
Both `anyhow` and `thiserror` support `no_std` with `default-features = false`:
|
|
|
|
```toml
|
|
anyhow = { version = "1.0", default-features = false }
|
|
thiserror = { version = "2.0", default-features = false }
|
|
```
|
|
|
|
Error types must use `alloc::string::String` instead of `std::string::String`
|
|
and avoid `std::io::Error` without a feature gate.
|