Files
regorus/docs/knowledge/error-handling-migration.md
copilot-swe-agent[bot] 524aab5528 Add knowledge docs, agent definitions, and skill files
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>
2026-04-25 21:20:10 +00:00

5.7 KiB

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!():

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:

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:

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:

// 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:

#[derive(Error, Debug, Clone, PartialEq)]

Clone enables error propagation without ownership transfer. PartialEq enables testing error conditions precisely.

5. Type alias for ergonomics:

pub type Result<T> = core::result::Result<T, VmError>;

6. Bridge from anyhow:

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:

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):

// 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:
// ✗ 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:

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.