Skip to content

RFC 013: Error Code Specification

Summary

This RFC proposes a classification specification for YaoXiang compiler error codes, using a single-level numbering system similar to Rust, with JSON resource files for multi-language support, and a yaoxiang explain command to provide error explanations.

Motivation

Why do we need standardized error codes?

  1. User experience: Users can quickly identify the error type and severity by looking at the error code
  2. Documentation organization: Grouping by category facilitates writing and maintaining error reference documentation
  3. Tool integration: IDE/LSP can provide quick fix suggestions and documentation links based on error codes
  4. Internationalization support: Separating error messages from codes facilitates multi-language translation

Design Goals

  • Concise: Single-level numbering, users don't need to memorize complex classification rules
  • Friendly: Error message format similar to Rust, with help information and examples
  • Extensible: Resource file-driven, easy to add new errors and new languages
  • Tool-friendly: explain command + JSON output, supports IDE/LSP integration

Proposal

Core Design: Single-Level Numbering System

Uses a four-digit numbering scheme, grouped by compilation stage:

Exxxx
││││
│││└── Sequence number (000-999)
││└─── Compilation stage (0-9)
└───── Fixed prefix 'E'

Stage Division

StageRangeDescription
0E0xxxLexical and syntax analysis
1E1xxxType checking
2E2xxxSemantic analysis
3E3xxxCode generation
4E4xxxGenerics and traits
5E5xxxModules and imports
6E6xxxRuntime errors
7E7xxxI/O and system errors
8E8xxxInternal compiler errors
9E9xxxReserved/experimental

Error Category Enum

rust
/// Error category
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
    Lexer,      // E0xxx: Lexical and syntax analysis
    Parser,     // E0xxx: Parser errors
    TypeCheck,  // E1xxx: Type checking
    Semantic,   // E2xxx: Semantic analysis
    Generic,    // E4xxx: Generics and traits
    Module,     // E5xxx: Modules and imports
    Runtime,    // E6xxx: Runtime errors
    Io,         // E7xxx: I/O and system errors
    Internal,   // E8xxx: Internal compiler errors
}

Error Code Definition and Universal Builder

Core principle: Separate error code definition from display text

  • ErrorCodeDefinition: Error code metadata (code, category, template), without display text
  • i18n/*.json: Display text for each language (title, message, help)
  • DiagnosticBuilder: Universal builder, replacing trait-per-error design

Error Code Definition

rust
// diagnostic/codes/mod.rs

use crate::util::span::Span;
use crate::util::diagnostic::{Diagnostic, Severity};

/// Error code definition (metadata only, display text in i18n files)
#[derive(Debug, Clone, Copy)]
pub struct ErrorCodeDefinition {
    pub code: &'static str,
    pub category: ErrorCategory,
    pub message_template: &'static str,  // Message template, supports {param} placeholders
}

/// Universal diagnostic builder
pub struct DiagnosticBuilder {
    code: &'static str,
    message_template: &'static str,
    params: Vec<(&'static str, String)>,
    span: Option<Span>,
}

impl DiagnosticBuilder {
    pub fn new(code: &'static str, template: &'static str) -> Self {
        Self {
            code,
            message_template: template,
            params: Vec::new(),
            span: None,
        }
    }

    /// Add template parameter
    pub fn param(mut self, key: &'static str, value: impl Into<String>) -> Self {
        self.params.push((key, value.into()));
        self
    }

    /// Set location
    pub fn at(mut self, span: Span) -> Self {
        self.span = Some(span);
        self
    }

    /// Build Diagnostic (template rendering done at compile time)
    pub fn build(&self, i18n: &I18nRegistry) -> Diagnostic {
        // Check that all {key} in template have corresponding parameters
        self.validate_params();

        let message = i18n.render(self.message_template, &self.params);
        let help = self.help(i18n);

        Diagnostic {
            severity: Severity::Error,
            code: self.code.to_string(),
            message,
            help,
            span: self.span,
            related: Vec::new(),
        }
    }
}

Convenience Methods for Each Error Code

rust
// diagnostic/codes/e1xxx.rs

impl ErrorCodeDefinition {
    /// E1001 Unknown variable
    pub fn unknown_variable(name: &str) -> DiagnosticBuilder {
        let def = Self::find("E1001").unwrap();
        DiagnosticBuilder::new(def.code, def.message_template)
            .param("name", name)
    }

    /// E1002 Type mismatch
    pub fn type_mismatch(expected: &str, found: &str) -> DiagnosticBuilder {
        let def = Self::find("E1002").unwrap();
        DiagnosticBuilder::new(def.code, def.message_template)
            .param("expected", expected)
            .param("found", found)
    }
}

Usage Example

rust
// checking/mod.rs

use crate::util::diagnostic::codes::{ErrorCodeDefinition, E1001};

// Simplified way
return Err(E1001::unknown_variable(&var_name)
    .at(span)
    .build(&i18n_registry));

// Manual way
return Err(ErrorCodeDefinition::find("E1001")
    .builder()
    .param("name", var_name)
    .at(span)
    .build(&i18n_registry));

Error Code Definition Example

rust
// diagnostic/codes/e1xxx.rs

pub static E1XXX: &[ErrorCodeDefinition] = &[
    ErrorCodeDefinition {
        code: "E1001",
        category: ErrorCategory::TypeCheck,
        message_template: "Unknown variable: '{name}'",
    },
    ErrorCodeDefinition {
        code: "E1002",
        category: ErrorCategory::TypeCheck,
        message_template: "Expected type '{expected}', found type '{found}'",
    },
    // ... other error codes
];

Design Advantages

FeatureDescription
Single BuilderOne DiagnosticBuilder works for all error codes
Type safeConvenience methods ensure parameter correctness
Self-documentingE1001::unknown_variable(name) is self-explanatory
Template separationMessage templates separated from code, easy for i18n
Zero runtime overheadCompile-time rendering, AOT binary has no lookup table

Error Macro Simplification

error! Macro (Auto-inject Context)

rust
/// Macro that automatically retrieves span and i18n config at compile time
macro_rules! error {
    ($code:ident, $($key:ident = $value:expr),* $(,)?) => {
        $code()
            $(.$key($value))*
            .at(crate::util::span::Span::current())
            .build(crate::util::diagnostic::I18nRegistry::current())
    };
}

/// Usage: just pass parameters, span and i18n are automatically injected
return Err(error!(E1001, name = var_name));
return Err(error!(E1002, expected = "bool", found = cond_ty));

Manual Builder Usage

rust
// When manual control is needed
E1001::unknown_variable(&var_name)
    .at(my_span)           // Custom span
    .build(&custom_i18n)   // Custom i18n

Detailed Design

Error Code List

E0xxx: Lexical and Syntax Analysis

CodeError TypeDescription
E0001Invalid characterSource code contains illegal character
E0002Invalid number literalIncorrect number literal format
E0003Unterminated stringMulti-line string missing closing quote
E0004Invalid character literalIncorrect character literal
E0010Expected tokenParser expected specific token
E0011Unexpected tokenEncountered unexpected token
E0012Invalid syntaxExpression/statement syntax error
E0013Mismatched bracketsParentheses, brackets, or braces don't match
E0014Missing semicolonMissing semicolon at end of statement

E1xxx: Type Checking

CodeError TypeDescription
E1001Unknown variableReferenced variable is not defined
E1002Type mismatchExpected type doesn't match actual type
E1003Unknown typeReferenced type doesn't exist
E1010Parameter count mismatchFunction call parameter count doesn't match definition
E1011Parameter type mismatchParameter type checking failed
E1012Return type mismatchFunction return type error
E1013Function not foundCalling undefined function
E1020Cannot infer typeCannot infer type from context
E1021Type inference conflictMultiple constraints cause type contradiction
E1030Pattern non-exhaustivematch expression doesn't cover all cases
E1031Unreachable patternPattern that can never match
E1040Operation not supportedType doesn't support this operation
E1041Index out of boundsArray/list index out of range
E1042Field not foundAccessing non-existent struct field

E2xxx: Semantic Analysis

CodeError TypeDescription
E2001Scope errorVariable not in current scope
E2002Duplicate definitionDuplicate definition in same scope
E2003Lifetime errorLifetime constraint not satisfied
E2010Immutable assignmentAttempting to modify immutable variable
E2011Uninitialized useUsing uninitialized variable
E2012Mutability conflictUsing mutable reference in immutable context

E4xxx: Generics and Traits

CodeError TypeDescription
E4001Generic parameter mismatchGeneric parameter count/type mismatch
E4002Trait bound violatedTrait constraint not satisfied
E4003Associated type errorAssociated type definition/use error
E4004Duplicate trait implementationDuplicate implementation of same trait
E4005Trait not foundRequired trait not found
E4006Sized bound violatedSized constraint not satisfied

E5xxx: Modules and Imports

CodeError TypeDescription
E5001Module not foundImported module doesn't exist
E5002Cyclic importCircular dependency between modules
E5003Symbol not exportedAttempting to access non-exported symbol
E5004Invalid module pathIncorrect module path format
E5005Private accessAccessing private symbol

E6xxx: Runtime Errors

CodeError TypeDescription
E6001Division by zeroInteger division by zero
E6002Assertion failedassert! macro failed
E6003Arithmetic overflowArithmetic operation overflow
E6004Stack overflowStack space exhausted
E6005Heap allocation failedMemory allocation failed
E6006Runtime index out of boundsRuntime index out of range
E6007Type cast failedAttempting to cast type to incompatible type

E7xxx: I/O and System Errors

CodeError TypeDescription
E7001File not foundAttempting to read non-existent file
E7002Permission deniedInsufficient file permissions
E7003I/O errorGeneral I/O error
E7004Network errorNetwork operation failed

E8xxx: Internal Compiler Errors

CodeError TypeDescription
E8001Internal compiler errorCompiler internal error
E8002Codegen errorIR/bytecode generation failed
E8003Unimplemented featureUsing unimplemented feature
E8004Optimization errorCompiler optimization error

Multi-Language Resource Files

Resource File Format

json
// diagnostic/codes/i18n/en.json
{
  "E1001": {
    "title": "Unknown variable",
    "message": "Referenced variable is not defined",
    "template": "Unknown variable: '{name}'",
    "help": "Check if the variable name is spelled correctly, or define it first",
    "example": "x = 100;",
    "error_output": "error[E1001]: Unknown variable: 'x'\n  --> example.yx:1:1\n   |\n 1 | print(x)\n   | ^ unknown variable 'x'"
  },
  "E1002": {
    "title": "Type mismatch",
    "message": "Expected type does not match actual type",
    "template": "Expected type '{expected}', found type '{found}'",
    "help": "Use the correct type or add a type conversion",
    "example": "x: Int = \"hello\";",
    "error_output": "error[E1002]: Type mismatch\n  --> example.yx:1:12\n   |\n 1 | x: Int = \"hello\";\n   |            ^ expected 'Int', found 'String'"
  }
}
json
// diagnostic/codes/i18n/zh.json
{
  "E1001": {
    "title": "未知变量",
    "message": "引用的变量未定义",
    "template": "未知变量:'{name}'",
    "help": "检查变量名是否拼写正确,或先定义它",
    "example": "x = 100;",
    "error_output": "error[E1001]: 未知变量:'x'\n  --> example.yx:1:1\n   |\n 1 | print(x)\n   | ^ 未知变量 'x'"
  },
  "E1002": {
    "title": "类型不匹配",
    "message": "期望类型与实际类型不匹配",
    "template": "期望类型 '{expected}',实际类型 '{found}'",
    "help": "使用正确的类型或添加类型转换",
    "example": "x: Int = \"hello\";",
    "error_output": "error[E1002]: 类型不匹配\n  --> example.yx:1:12\n   |\n 1 | x: Int = \"hello\";\n   |            ^ 期望 'Int',找到 'String'"
  }
}

I18nRegistry Implementation

rust
// diagnostic/codes/i18n/mod.rs

/// i18n display text registry (loaded from JSON at compile time, zero lookup at runtime)
pub struct I18nRegistry {
    /// Titles
    titles: HashMap<&'static str, &'static str>,
    /// Descriptions
    messages: HashMap<&'static str, &'static str>,
    /// Help messages
    helps: HashMap<&'static str, &'static str>,
    /// Example code
    examples: HashMap<&'static str, &'static str>,
    /// Error output examples
    error_outputs: HashMap<&'static str, &'static str>,
}

/// Single error code information
#[derive(Clone, Copy)]
pub struct ErrorInfo<'a> {
    pub title: &'a str,
    pub message: &'a str,
    pub help: &'a str,
    pub example: Option<&'a str>,
    pub error_output: Option<&'a str>,
}

impl I18nRegistry {
    /// Get registry based on language code
    pub fn new(lang: &str) -> Self {
        match lang {
            "zh" => Self::zh(),
            _ => Self::en(),
        }
    }

    /// Get error information
    pub fn get_info(&self, code: &str) -> Option<ErrorInfo<'_>> {
        Some(ErrorInfo {
            title: self.titles.get(code)?,
            message: self.messages.get(code)?,
            help: self.helps.get(code)?,
            example: self.examples.get(code).copied(),
            error_output: self.error_outputs.get(code).copied(),
        })
    }

    /// Render template (done at compile time, zero overhead at runtime)
    pub fn render(&self, template: &'static str, params: &[(&str, String)]) -> String {
        let mut result = String::with_capacity(template.len() + 64);
        let mut chars = template.chars().peekable();

        while let Some(c) = chars.next() {
            if c == '{' {
                let mut key = String::new();
                while let Some(&c) = chars.peek() {
                    if c == '}' {
                        chars.next();
                        if let Some((_, value)) = params.iter().find(|(k, _)| k == &key) {
                            result.push_str(value);
                        } else {
                            result.push_str(&format!("{{{}}}", key));
                        }
                        break;
                    }
                    key.push(c);
                    chars.next();
                }
            } else {
                result.push(c);
            }
        }
        result
    }
}

Template Placeholders

Predefined Placeholders (Common)
PlaceholderPurposeExample
{name}Variable/type/trait name etc. identifierUnknown variable: '{name}'
{expected}Expected typeExpected type '{expected}'
{found}Actual/found type, found type '{found}'
{method}Method nameMethod {method} is not a function
{trait}Trait nameCannot find trait: {trait}
{path}Module pathInvalid path: {path}
{ty}Type expressionInvalid type: {ty}
{message}Internal error messageInternal error: {message}
Arbitrary Key Support

params supports arbitrary keys, not limited to predefined. Callers can pass any key:

rust
// Using arbitrary key
E1001::unknown_variable(&var_name)
    .param("location", "global scope")
    .param("hint", "try declaring it first")
    .at(span)
    .build(&i18n);

// Template definition
"Unknown variable: '{name}' at {location}. {hint}"

Note: Not all error codes use placeholders. Some error codes (like E0001) are static messages that don't need parameters.

Language Priority

1. yaoxiang.toml [language.default]
2. ~/.yaoxiang/yaoxiang.toml [language.default]
3. Default value: en

yaoxiang.toml Configuration

Project-Level Configuration

toml
# yaoxiang.toml
[project]
name = "my-project"
version = "0.1.0"

[language]
# Error message language, options: en, zh, ja, ...
default = "zh"

User-Level Configuration

toml
# ~/.yaoxiang/yaoxiang.toml
[language]
default = "zh"

Compile-Time Language Selection

1. Read language.default from project-level yaoxiang.toml
2. If not configured, read user-level ~/.yaoxiang/yaoxiang.toml
3. If neither is configured, default to "en"
4. Compiler creates I18nRegistry based on selected language (once)
5. All errors use this I18nRegistry to render messages

Key to Zero Lookup Overhead

Rendering happens when compiling the user's project, not at runtime.

┌─────────────────────────────────────────────────────────────────────────┐
│  Stage 1: Rust compiles YaoXiang compiler                               │
│                                                                           │
│  JSON packed into compiler binary                                        │
│  Purpose: explain command can directly read i18n data                  │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│  Stage 2: YaoXiang compiles user's project (rendering happens here)     │
│                                                                           │
│  When error! macro is called:                                            │
│  1. Read yaoxiang.toml to get language preference                        │
│  2. Load corresponding language's i18n JSON from compiler binary        │
│  3. template + params → render() → "Unknown variable: 'x'"              │
│  4. Diagnostic.message = rendered string                                │
│                                                                           │
│  AOT binary stores final string directly, no template, no lookup table   │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│  Stage 3: User program runtime                                           │
│                                                                           │
│  println!("{}", diagnostic.message)                                     │
│  // Directly output final string, no lookup at all                       │
└─────────────────────────────────────────────────────────────────────────┘
ComponentResponsibilityRendering Time
I18nRegistryProvides templates and display textWhen compiling user's project
DiagnosticBuilder.render()template + params → final stringWhen compiling user's project
Diagnostic.messageRendered stringStores final result
AOT binaryContains final stringUsed directly at runtime

Error Message Format

Error messages use the following format:

error[E####]: <short description>
  --> <file>:<line>:<column>
   <line> | <code snippet>
          ^^^<highlight>

Complete Example

error[E1001]: Unknown variable: x
  --> src/main.yx:5:12
   5 |   print(x)
          ^
          help: Did you mean to define it?

Severity Levels

Error severity is managed through the DiagnosticLevel enum, decoupled from error code numbering:

rust
pub enum DiagnosticLevel {
    Error,    // Causes compilation failure
    Warning,  // Doesn't affect compilation, but recommended to fix
    Note,     // Additional information
    Help,     // Fix suggestion
}
LevelPrefixDescription
Errorerror[E####]:Causes compilation failure
Warningwarning[E####]:Doesn't affect compilation
Notenote[E####]:Additional information
Helphelp[E####]:Fix suggestion

yaoxiang explain Command

Command Syntax

bash
yaoxiang explain <ERROR_CODE> [OPTIONS]

Options

OptionDescription
--lang <code>Specify language (en-US, zh-CN, default en-US)
--jsonJSON format output (for IDE/LSP use)
--json-prettyFormatted JSON output
--examplesShow only example code
--helpDisplay help information

Usage Examples

bash
# Default English
$ yaoxiang explain E1001
error[E1001]: Unknown variable: {name}
  --> <file>:<line>:<col>

Help: Did you mean to define it?

Example:
  let {name} = value;

# Chinese output
$ yaoxiang explain E1001 --lang zh
error[E1001]: 未知变量: {name}
  --> <file>:<line>:<col>

帮助: 你是否想要定义它?

示例:
  let {name} = value;

# JSON output (LSP integration)
$ yaoxiang explain E1001 --json
{
  "code": "E1001",
  "message": "Unknown variable: {name}",
  "help": "Did you mean to define it?",
  "examples": ["let {name} = value;"],
  "language": "en-US"
}

JSON Output Format

json
{
  "code": "E1001",
  "message": "Unknown variable: {name}",
  "help": "Did you mean to define it?",
  "examples": ["let {name} = value;"],
  "language": "en-US"
}

Backward Compatibility

Since this RFC designs the error code system from scratch, there is no backward compatibility issue.

Future Migration Strategy (for reference in subsequent versions):

  1. Maintain mapping from old error codes to new error codes
  2. Display both old and new codes during migration period
  3. Provide deprecation schedule

Implementation Strategy

Phase One: Error Code Infrastructure

  1. Create src/diagnostics/ directory structure
  2. Implement ErrorCode enum
  3. Implement Diagnostic and DiagnosticLevel
  4. Create resource file directory and sample JSON

Phase Two: explain Command

  1. Implement yaoxiang explain CLI command
  2. Support --lang and --json options
  3. Integrate resource file loading
  4. Implement parameter template rendering

Phase Three: Compile-Time Integration

  1. Update all error reporting points to use new system
  2. Implement message template parameter injection
  3. Add language priority logic
  4. Unit test coverage

Phase Four: IDE/LSP Integration

  1. LSP server integrates explain JSON output
  2. Display error code links in IDE
  3. Hover to show error explanation
  4. Quick fix suggestions

Appendix

Complete Error Code Quick Reference

RangeCategory
E0xxxLexical and syntax analysis
E1xxxType checking
E2xxxSemantic analysis
E3xxxCode generation
E4xxxGenerics and traits
E5xxxModules and imports
E6xxxRuntime errors
E7xxxI/O and system errors
E8xxxInternal compiler errors
E9xxxReserved

Supported Languages

CodeLanguageStatus
en-USEnglish (US)Default
zh-CNSimplified ChinesePlanned

Error Message Example Comparison

# English (en-US)
error[E1001]: Unknown variable: x
  --> src/main.yx:5:12
   5 |   print(x)
          ^
          help: Did you mean to define it?

# Chinese (zh-CN)
error[E1001]: 未知变量: x
  --> src/main.yx:5:12
   5 |   print(x)
          ^
          帮助: 你是否想要定义它?

References