Diagnostics Overview

The diagnostics system provides error reporting, warnings, and code fix suggestions for the Ori compiler.

Location

The diagnostics system spans multiple crates:

compiler/
├── ori_diagnostic/               # Core diagnostic types (separate crate)
│   └── src/
│       ├── lib.rs                    # Module organization and re-exports
│       ├── error_code.rs             # ErrorCode enum, as_str(), Display
│       ├── diagnostic.rs             # Diagnostic, Label, Severity, Applicability, Suggestion
│       ├── guarantee.rs              # ErrorGuaranteed type-level proof
│       ├── queue.rs                  # DiagnosticQueue for deduplication/limits
│       ├── span_utils.rs             # Line/column computation from spans
│       ├── errors/                   # Embedded error documentation for --explain
│       │   ├── mod.rs                    # ErrorDocs registry
│       │   ├── E0001.md                  # Error documentation files
│       │   ├── E0002.md
│       │   └── ...                       # (35+ error codes documented)
│       ├── emitter/
│       │   ├── mod.rs                    # Emitter trait, trailing_comma() helper
│       │   ├── terminal.rs               # Terminal output
│       │   ├── json.rs                   # JSON output
│       │   └── sarif.rs                  # SARIF format (BTreeSet for rule dedup)
│       └── fixes/
│           ├── mod.rs                    # Code fix system
│           └── registry.rs               # Fix registry
├── ori-macros/                   # Proc-macro crate for diagnostic derives
│   └── src/
│       ├── lib.rs                    # Derive macro exports
│       ├── diagnostic.rs             # #[derive(Diagnostic)] implementation
│       └── subdiagnostic.rs          # #[derive(Subdiagnostic)] implementation
└── oric/src/
    ├── problem/                  # Problem types (specific to compiler phases)
    │   ├── mod.rs                    # Problem enum (Parse, Type, Semantic variants)
    │   └── semantic.rs               # SemanticProblem enum, DefinitionKind
    └── reporting/                # Diagnostic rendering (Problem → Diagnostic)
        ├── mod.rs                    # Render trait, render_all, Report type
        ├── parse.rs                  # ParseProblem rendering
        ├── semantic.rs               # SemanticProblem rendering
        └── type_errors.rs            # TypeProblem rendering

**Note:** The `problem/` and `reporting/` modules have an intentional 1:1 coupling.
Each problem variant in `problem/mod.rs` has a corresponding `Render` implementation
in `reporting/`. This separation keeps "what went wrong" (Problem) distinct from
"how to display it" (Diagnostic), while the 1:1 mapping ensures every problem
gets a tailored error message. Adding a new problem type requires adding its
renderer in the corresponding reporting module.

The ori_diagnostic crate is organized into focused submodules: error_code.rs (ErrorCode enum), diagnostic.rs (Diagnostic, Label, Severity, Applicability, Suggestion types), and guarantee.rs (ErrorGuaranteed). The lib.rs re-exports all public types. It depends only on ori_ir (for Span). The proc-macros in ori-macros generate implementations of the IntoDiagnostic trait.

Design Goals

  1. Helpful messages - Clear, actionable error descriptions
  2. Machine-readable - JSON/SARIF for tooling integration
  3. Code fixes - Automatic fix suggestions
  4. Error codes - Stable identifiers for documentation
  5. Error guarantees - Type-level proof that errors were reported

ErrorGuaranteed

The ErrorGuaranteed type provides type-level proof that at least one error was emitted. This prevents “forgotten” error conditions where code fails silently without reporting an error.

/// Proof that at least one error was emitted.
/// Can only be created by emitting an error via DiagnosticQueue.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct ErrorGuaranteed(());

impl ErrorGuaranteed {
    pub(crate) fn new() -> Self;  // Only callable from queue.rs
    pub fn from_error_count(count: usize) -> Option<Self>;  // For downstream
    pub fn new_for_downstream() -> Self;  // When errors verified elsewhere
}

Usage Pattern

// Functions return ErrorGuaranteed to prove they reported errors
fn type_check(&mut self) -> Result<TypedModule, ErrorGuaranteed> {
    if let Some(error) = self.check_for_errors() {
        // Can only get ErrorGuaranteed by actually emitting
        let guarantee = self.queue.emit_error(error.to_diagnostic(), line, col);
        return Err(guarantee);
    }
    Ok(self.build_typed_module())
}

DiagnosticQueue Methods

impl DiagnosticQueue {
    /// Emit error and get proof it was emitted.
    pub fn emit_error(&mut self, diag: Diagnostic, line: u32, col: u32) -> ErrorGuaranteed;

    /// Emit error with position computed from source.
    pub fn emit_error_with_source(&mut self, diag: Diagnostic, source: &str) -> ErrorGuaranteed;

    /// Check if any errors were emitted.
    pub fn has_errors(&self) -> Option<ErrorGuaranteed>;
}

Salsa Compatibility

ErrorGuaranteed implements Copy, Clone, Eq, Hash for use in Salsa query results:

#[salsa::tracked]
fn typed(db: &dyn Db, file: SourceFile) -> Result<TypedModule, ErrorGuaranteed>

Error Code Ranges

RangeCategoryExamples
E0xxxLexerE0001: Invalid character
E1xxxParserE1001: Unexpected token
E2xxxType checkerE2001: Type mismatch
E3xxxPatternsE3001: Unknown pattern
E9xxxInternalE9001: Compiler bug, E9002: Too many errors

DiagnosticQueue

The DiagnosticQueue provides Go-style error handling with deduplication, limits, and sorting:

pub struct DiagnosticQueue {
    diagnostics: Vec<QueuedDiagnostic>,
    error_count: usize,
    config: DiagnosticConfig,
    // Deduplication state
    last_syntax_line: Option<u32>,
    last_error: Option<(u32, String)>,
    has_hard_error: bool,
}

pub struct DiagnosticConfig {
    pub error_limit: usize,      // Default: 10 (0 = unlimited)
    pub filter_follow_on: bool,  // Default: true
    pub deduplicate: bool,       // Default: true
}

Features

  1. Error Limits - Stop after N errors (default 10) to avoid overwhelming output
  2. Deduplication - Same-line syntax errors and same-message errors are collapsed
  3. Follow-on Filtering - Errors caused by previous errors (e.g., “invalid operand”) are suppressed
  4. Soft Error Suppression - After a hard error, soft errors (inference failures) are hidden
  5. Position-based Sorting - Errors are sorted by source location for consistent output

Usage

let config = DiagnosticConfig::default();
let mut queue = DiagnosticQueue::with_config(config);

// Add diagnostics with source for line computation
queue.add_with_source(diagnostic, source, is_soft);

// Check if error limit reached
if queue.limit_reached() {
    // Stop processing
}

// Flush sorted diagnostics
let sorted = queue.flush();

Integration with TypeChecker

The type checker optionally uses DiagnosticQueue for production builds:

// With queue (production)
let typed = type_check_with_source(&parse_result, interner, source.clone());

// With custom config
let config = DiagnosticConfig { error_limit: 5, ..Default::default() };
let typed = type_check_with_config(&parse_result, interner, source, config);

Diagnostic Rendering

The rendering system converts structured Problem types into user-facing Diagnostic messages. This separates “what went wrong” (Problem) from “how to display it” (Diagnostic).

Render Trait

The Render trait provides the conversion interface:

pub trait Render {
    fn render(&self) -> Diagnostic;
}

impl Render for Problem {
    fn render(&self) -> Diagnostic {
        match self {
            Problem::Parse(p) => p.render(),
            Problem::Type(p) => p.render(),
            Problem::Semantic(p) => p.render(),
        }
    }
}

Module Organization

Each problem category has its own rendering module:

ModuleProblem TypeError Codes
parse.rsParseProblemE1xxx (parser errors)
semantic.rsSemanticProblemE2xxx (name resolution, duplicates)
type_errors.rsTypeProblemE2xxx (type mismatches, inference)

This separation follows the Single Responsibility Principle—each module focuses on rendering one category of problems with domain-specific context and suggestions.

Helper Functions

/// Render all problems to diagnostics.
pub fn render_all(problems: &[Problem]) -> Vec<Diagnostic>;

/// Process type errors through the diagnostic queue.
pub fn process_type_errors(
    errors: Vec<TypeCheckError>,
    source: &str,
    config: Option<DiagnosticConfig>,
) -> Vec<Diagnostic>;

Diagnostic Structure

pub struct Diagnostic {
    /// Error code (e.g., E2001)
    pub code: ErrorCode,

    /// Severity level
    pub severity: Severity,

    /// Main message
    pub message: String,

    /// Labeled spans showing where the error occurred
    pub labels: Vec<Label>,

    /// Additional notes providing context
    pub notes: Vec<String>,

    /// Simple text suggestions (human-readable)
    pub suggestions: Vec<String>,

    /// Structured suggestions with spans and applicability (for `ori fix`)
    pub structured_suggestions: Vec<Suggestion>,
}

pub enum Severity {
    Error,
    Warning,
    Note,
    Help,
}

Structured Suggestions

Structured suggestions enable ori fix to auto-apply fixes:

/// Applicability level for code suggestions
pub enum Applicability {
    /// Safe to auto-apply (typos, missing delimiters)
    MachineApplicable,

    /// Might be wrong (type conversions, imports)
    MaybeIncorrect,

    /// Contains placeholders needing user input
    HasPlaceholders,

    /// Unknown confidence level
    Unspecified,
}

pub struct Suggestion {
    pub message: String,
    pub substitutions: Vec<Substitution>,
    pub applicability: Applicability,
}

pub struct Substitution {
    pub span: Span,
    pub snippet: String,
}

Usage:

// Machine-applicable fix (safe to auto-apply)
Diagnostic::error(ErrorCode::E1001)
    .with_message("missing semicolon")
    .with_fix("add semicolon", span, ";")

// Maybe-incorrect fix (needs human review)
Diagnostic::error(ErrorCode::E2001)
    .with_maybe_fix("convert to int", span, "int(x)")

Example Output

Terminal output:

error[E2001]: type mismatch
 --> src/mainsi:10:15
   |
10 |     let x: int = "hello"
   |            ---   ^^^^^^^ expected int, found str
   |            |
   |            expected due to this annotation
   |
   = help: consider using int() to convert

Key Components

ErrorCode

pub enum ErrorCode {
    // Lexer
    E0001,  // Invalid character
    E0002,  // Unterminated string

    // Parser
    E1001,  // Unexpected token
    E1002,  // Expected expression
    E1003,  // Missing closing delimiter

    // Type checker
    E2001,  // Type mismatch
    E2002,  // Undefined variable
    E2003,  // Missing capability

    // Patterns
    E3001,  // Unknown pattern
    E3002,  // Missing required argument

    // Internal
    E9001,  // Internal compiler error
    E9002,  // Too many errors
}

Span Utilities

The span_utils module provides line/column computation for error positioning:

/// Compute 1-based line number from span and source.
pub fn line_number(source: &str, span: Span) -> u32;

/// Compute line number from byte offset.
pub fn line_from_offset(source: &str, offset: u32) -> u32;

/// Convert byte offset to (line, column) tuple.
pub fn offset_to_line_col(source: &str, offset: u32) -> (usize, usize);

These are used by DiagnosticQueue for position-based deduplication and sorting.

Problem

pub enum Problem {
    // Parser problems
    UnexpectedToken { expected: Vec<TokenKind>, found: TokenKind },
    UnterminatedString,

    // Type problems
    TypeMismatch { expected: Type, found: Type },
    UndefinedVariable { name: Name },
    MissingCapability { required: Capability },

    // Pattern problems
    UnknownPattern { name: Name },
    MissingArgument { pattern: Name, arg: &'static str },
}

Diagnostic Derive Macros

The ori-macros crate provides derive macros for declarative diagnostic definitions:

#[derive(Diagnostic)]
#[diag(E2001, "type mismatch: expected `{expected}`, found `{found}`")]
pub struct TypeMismatch {
    #[primary_span]
    #[label("expected `{expected}`")]
    pub span: Span,
    pub expected: String,
    pub found: String,
    #[suggestion("convert with `int({name})`", code = "int({name})", applicability = "maybe-incorrect")]
    pub conversion_span: Option<Span>,
}

// Usage:
let err = TypeMismatch { span, expected: "int".into(), found: "str".into(), conversion_span: None };
let diagnostic = err.into_diagnostic();

Supported attributes:

AttributeLevelDescription
#[diag(CODE, "msg")]StructError code and message template
#[primary_span]FieldMain error location
#[label("msg")]FieldLabel for a span
#[note("msg")]FieldAdditional note
#[suggestion(...)]FieldStructured fix suggestion

Subdiagnostics can be added via #[derive(Subdiagnostic)]:

#[derive(Subdiagnostic)]
#[label("this type was expected")]
pub struct ExpectedTypeLabel {
    #[primary_span]
    pub span: Span,
}

Emitters

Output formats:

FormatUse Case
TerminalHuman-readable, colored output
JSONIDE integration, tooling
SARIFStatic analysis tools

Error Documentation System

The errors/ directory contains embedded markdown documentation for each error code, accessible via ori --explain <code>.

ErrorDocs Registry

pub struct ErrorDocs;

impl ErrorDocs {
    /// Get documentation for an error code.
    pub fn get(code: ErrorCode) -> Option<&'static str>;

    /// Get all documented error codes.
    pub fn all_codes() -> impl Iterator<Item = ErrorCode>;

    /// Check if an error code has documentation.
    pub fn has_docs(code: ErrorCode) -> bool;
}

Documentation Format

Each error code has a markdown file (e.g., E2001.md) with:

# E2001: Type Mismatch

An expression has a different type than expected in the given context.

## Example

```ori
let x: int = "hello"  // error: expected `int`, found `str`

Common Causes

  1. Assigning wrong type to annotated variable
  2. Return type doesn’t match function signature

Solutions

  • Remove type annotation if inference should determine the type
  • Convert the value explicitly: int(value)

### Adding New Documentation

1. Create a new file `EXXXX.md` in `compiler/ori_diagnostic/src/errors/`
2. Add an entry to the `DOCS` array in `errors/mod.rs`:
   ```rust
   (ErrorCode::EXXXX, include_str!("EXXXX.md")),
  1. Run cargo build to embed the new documentation

CLI Integration

$ ori --explain E2001
# E2001: Type Mismatch

An expression has a different type than expected...