Skip to content

RFC 013: Стандарт кодов ошибок

Аннотация

Настоящий RFC предлагает стандарт классификации кодов ошибок компилятора YaoXiang, использующий одноуровневую систему нумерации в стиле Rust совместно с JSON-файлами ресурсов для поддержки многоязычности, а также команду yaoxiang explain для предоставления объяснений ошибок.

Мотивация

Зачем нужны стандартизированные коды ошибок?

  1. Пользовательский опыт: пользователи могут быстро определить тип и серьёзность ошибки по коду ошибки
  2. Организация документации: группировка по категориям упрощает написание и поддержку справочной документации по ошибкам
  3. Интеграция инструментов: IDE/LSP могут предоставлять предложения по быстрому исправлению и ссылки на документацию на основе кодов ошибок
  4. Поддержка интернационализации: разделение сообщений об ошибках и кодов упрощает перевод на другие языки

Цели проектирования

  • Простота: одноуровневая нумерация, пользователям не нужно запоминать сложные правила классификации
  • Дружелюбность: формат сообщений об ошибках в стиле Rust с информацией о справке и примерами
  • Расширяемость: управление через файлы ресурсов, легко добавлять новые ошибки и новые языки
  • Дружелюбность к инструментам: команда explain + JSON-вывод, поддержка интеграции IDE/LSP

Предложение

Основной дизайн: одноуровневая система нумерации

Используется четырёхзначная нумерация, сгруппированная по этапам компиляции:

Exxxx
││││
│││└── Порядковый номер (000-999)
││└─── Этап компиляции (0-9)
└───── Фиксированный префикс 'E'

Разделение на этапы

ЭтапДиапазонОписание
0E0xxxЛексический и синтаксический анализ
1E1xxxПроверка типов
2E2xxxСемантический анализ
3E3xxxГенерация кода
4E4xxxОбобщения и trait-ы
5E5xxxМодули и импорт
6E6xxxОшибки времени выполнения
7E7xxxОшибки ввода-вывода и системы
8E8xxxВнутренние ошибки компилятора
9E9xxxЗарезервировано/экспериментальное

Перечисление категорий ошибок

rust
/// Категория ошибки
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
    Lexer,      // E0xxx: Лексический и синтаксический анализ
    Parser,     // E0xxx: Ошибки парсера
    TypeCheck,  // E1xxx: Проверка типов
    Semantic,   // E2xxx: Семантический анализ
    Generic,    // E4xxx: Обобщения и trait-ы
    Module,     // E5xxx: Модули и импорт
    Runtime,    // E6xxx: Ошибки времени выполнения
    Io,         // E7xxx: Ошибки ввода-вывода и системы
    Internal,   // E8xxx: Внутренние ошибки компилятора
}

Определение кодов ошибок и универсальный Builder

Основной принцип: разделение определений кодов ошибок и отображаемого текста

  • ErrorCodeDefinition: метаданные кода ошибки (code, category, template), без отображаемого текста
  • i18n/*.json: отображаемый текст для каждого языка (title, message, help)
  • DiagnosticBuilder: универсальный строитель, заменяющий проектирование trait-per-error

Определение кодов ошибок

rust
// diagnostic/codes/mod.rs

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

/// Определение кода ошибки (только метаданные, отображаемый текст в файлах i18n)
#[derive(Debug, Clone, Copy)]
pub struct ErrorCodeDefinition {
    pub code: &'static str,
    pub category: ErrorCategory,
    pub message_template: &'static str,  // Шаблон сообщения с поддержкой заполнителей {param}
}

/// Универсальный строитель диагностики
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,
        }
    }

    /// Добавить параметр шаблона
    pub fn param(mut self, key: &'static str, value: impl Into<String>) -> Self {
        self.params.push((key, value.into()));
        self
    }

    /// Установить позицию
    pub fn at(mut self, span: Span) -> Self {
        self.span = Some(span);
        self
    }

    /// Построить Diagnostic (рендеринг шаблона выполняется на этапе компиляции)
    pub fn build(&self, i18n: &I18nRegistry) -> Diagnostic {
        // Проверка, что все {key} в шаблоне имеют соответствующие параметры
        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(),
        }
    }
}

Краткие методы для каждого кода ошибки

rust
// diagnostic/codes/e1xxx.rs

impl ErrorCodeDefinition {
    /// E1001 Неизвестная переменная
    pub fn unknown_variable(name: &str) -> DiagnosticBuilder {
        let def = Self::find("E1001").unwrap();
        DiagnosticBuilder::new(def.code, def.message_template)
            .param("name", name)
    }

    /// E1002 Несоответствие типов
    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)
    }
}

Пример использования

rust
// checking/mod.rs

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

// Упрощённый способ
return Err(E1001::unknown_variable(&var_name)
    .at(span)
    .build(&i18n_registry));

// Ручной способ
return Err(ErrorCodeDefinition::find("E1001")
    .builder()
    .param("name", var_name)
    .at(span)
    .build(&i18n_registry));

Пример определения кода ошибки

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}'",
    },
    // ... Другие коды ошибок
];

Преимущества дизайна

ХарактеристикаОписание
Единый BuilderОдин DiagnosticBuilder универсален для всех кодов ошибок
ТипобезопасностьКраткие методы обеспечивают правильность параметров
Само-документацияE1001::unknown_variable(name) понятен с первого взгляда
Разделение шаблоновШаблоны сообщений отделены от кода, легко делать i18n
Нулевые накладные расходы времени выполненияРендеринг на этапе компиляции, AOT бинарный файл без поиска по таблицам

Упрощение макроса ошибок

Макрос error! (автоматическое внедрение контекста)

rust
/// Макрос для автоматического получения span и конфигурации i18n на этапе компиляции
macro_rules! error {
    ($code:ident, $($key:ident = $value:expr),* $(,)?) => {
        $code()
            $(.$key($value))*
            .at(crate::util::span::Span::current())
            .build(crate::util::diagnostic::I18nRegistry::current())
    };
}

/// Использование: достаточно передать параметры, span и i18n внедряются автоматически
return Err(error!(E1001, name = var_name));
return Err(error!(E1002, expected = "bool", found = cond_ty));

Ручное использование Builder

rust
// Когда нужен ручной контроль
E1001::unknown_variable(&var_name)
    .at(my_span)           // Пользовательский span
    .build(&custom_i18n)   // Пользовательский i18n

Детальный дизайн

Список кодов ошибок

E0xxx: Лексический и синтаксический анализ

КодТип ошибкиОписание
E0001Invalid characterИсходный код содержит недопустимый символ
E0002Invalid number literalНеправильный формат числового литерала
E0003Unterminated stringМногострочная строка без закрывающей кавычки
E0004Invalid character literalНеправильный символьный литерал
E0010Expected tokenОжидался определённый token во время синтаксического анализа
E0011Unexpected tokenВстречен неожиданный token
E0012Invalid syntaxСинтаксическая ошибка в выражении/операторе
E0013Mismatched bracketsНесовпадение круглых, квадратных или фигурных скобок
E0014Missing semicolonОтсутствует точка с запятой в конце оператора

E1xxx: Проверка типов

КодТип ошибкиОписание
E1001Unknown variableСсылка на неопределённую переменную
E1002Type mismatchОжидаемый тип не соответствует фактическому
E1003Unknown typeСсылка на несуществующий тип
E1010Parameter count mismatchКоличество параметров при вызове функции не соответствует определению
E1011Parameter type mismatchОшибка проверки типа параметра
E1012Return type mismatchНеправильный тип возвращаемого значения функции
E1013Function not foundВызов неопределённой функции
E1020Cannot infer typeТип не может быть выведен из контекста
E1021Type inference conflictПротиворечивые ограничения в нескольких местах приводят к противоречию типов
E1030Pattern non-exhaustiveВыражение match не покрывает все случаи
E1031Unreachable patternПаттерн, который никогда не сможет совпасть
E1040Operation not supportedТип не поддерживает данную операцию
E1041Index out of boundsИндекс массива/списка выходит за границы
E1042Field not foundОбращение к несуществующему полю структуры

E2xxx: Семантический анализ

КодТип ошибкиОписание
E2001Scope errorПеременная не находится в текущей области видимости
E2002Duplicate definitionПовторное определение в одной области видимости
E2003Lifetime errorОграничение времени жизни не удовлетворено
E2010Immutable assignmentПопытка изменить неизменяемую переменную
E2011Uninitialized useИспользование неинициализированной переменной
E2012Mutability conflictИспользование изменяемой ссылки в неизменяемом контексте

E4xxx: Обобщения и trait-ы

КодТип ошибкиОписание
E4001Generic parameter mismatchКоличество/тип обобщённых параметров не совпадает
E4002Trait bound violatedОграничение trait не удовлетворено
E4003Associated type errorОшибка определения/использования ассоциированного типа
E4004Duplicate trait implementationПовторная реализация одного trait
E4005Trait not foundНе удалось найти требуемый trait
E4006Sized bound violatedОграничение Sized не удовлетворено

E5xxx: Модули и импорт

КодТип ошибкиОписание
E5001Module not foundИмпортируемый модуль не существует
E5002Cyclic importЦиклическая зависимость между модулями
E5003Symbol not exportedПопытка доступа к неэкспортированному символу
E5004Invalid module pathНеправильный формат пути модуля
E5005Private accessДоступ к приватному символу

E6xxx: Ошибки времени выполнения

КодТип ошибкиОписание
E6001Division by zeroДеление целого числа на ноль
E6002Assertion failedМакрос assert! не прошёл
E6003Arithmetic overflowПереполнение при арифметических операциях
E6004Stack overflowИсчерпание пространства стека
E6005Heap allocation failedСбой выделения памяти
E6006Runtime index out of boundsВыход индекса за границы во время выполнения
E6007Type cast failedПопытка преобразовать тип в несовместимый

E7xxx: Ошибки ввода-вывода и системы

КодТип ошибкиОписание
E7001File not foundПопытка чтения несуществующего файла
E7002Permission deniedНедостаточно прав на файл
E7003I/O errorОбщая ошибка ввода-вывода
E7004Network errorСбой сетевой операции

E8xxx: Внутренние ошибки компилятора

КодТип ошибкиОписание
E8001Internal compiler errorВнутренняя ошибка компилятора
E8002Codegen errorСбой генерации IR/байткода
E8003Unimplemented featureИспользование нереализованной функции
E8004Optimization errorОшибка оптимизации компилятора

Файлы ресурсов для многоязычности

Формат файлов ресурсов

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/ru.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

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

/// Реестр отображаемого текста i18n (загружается из JSON на этапе компиляции, без поиска по таблицам во время выполнения)
pub struct I18nRegistry {
    /// Заголовки
    titles: HashMap<&'static str, &'static str>,
    /// Описания
    messages: HashMap<&'static str, &'static str>,
    /// Справочная информация
    helps: HashMap<&'static str, &'static str>,
    /// Примеры кода
    examples: HashMap<&'static str, &'static str>,
    /// Примеры вывода ошибок
    error_outputs: HashMap<&'static str, &'static str>,
}

/// Информация об одном коде ошибки
#[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 {
    /// Получить реестр по коду языка
    pub fn new(lang: &str) -> Self {
        match lang {
            "ru" => Self::ru(),
            "zh" => Self::zh(),
            _ => Self::en(),
        }
    }

    /// Получить информацию об ошибке
    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(),
        })
    }

    /// Рендеринг шаблона (выполняется на этапе компиляции, нулевые накладные расходы во время выполнения)
    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
    }
}

Заполнители шаблона

Предопределённые заполнители (часто используемые)
ЗаполнительНазначениеПример
{name}Имя переменной/типа/trait и др.Unknown variable: '{name}'
{expected}Ожидаемый типExpected type '{expected}'
{found}Фактический/найденный тип, found type '{found}'
{method}Имя методаMethod {method} is not a function
{trait}Имя traitCannot find trait: {trait}
{path}Путь к модулюInvalid path: {path}
{ty}Выражение типаInvalid type: {ty}'
{message}Внутреннее сообщение об ошибкеInternal error: {message}
Поддержка произвольных ключей

params поддерживает произвольные ключи, не ограничиваясь предопределёнными. Вызывающая сторона может передавать любые key:

rust
// Использование произвольного ключа
E1001::unknown_variable(&var_name)
    .param("location", "global scope")
    .param("hint", "try declaring it first")
    .at(span)
    .build(&i18n);

// Определение шаблона
"Unknown variable: '{name}' at {location}. {hint}"

Примечание: Не все коды ошибок используют заполнители. Некоторые коды ошибок (например, E0001) имеют статические сообщения без параметров.

Приоритет языков

1. yaoxiang.toml [language.default]
2. ~/.yaoxiang/yaoxiang.toml [language.default]
3. Значение по умолчанию: en

Конфигурация yaoxiang.toml

Конфигурация на уровне проекта

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

[language]
# Язык сообщений об ошибках: en, ru, zh, ja, ...
default = "ru"

Конфигурация на уровне пользователя

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

Выбор языка на этапе компиляции

1. Читается language.default из yaoxiang.toml проекта
2. Если не настроено, читается ~/.yaoxiang/yaoxiang.toml
3. Если оба не настроены, по умолчанию используется "en"
4. Компилятор создаёт I18nRegistry на основе выбранного языка (один раз)
5. Все ошибки используют этот I18nRegistry для рендеринга сообщений

Ключ к нулевым накладным расходам на поиск по таблицам

Рендеринг происходит при компиляции пользовательского проекта, а не во время выполнения.

┌─────────────────────────────────────────────────────────────────────────┐
│  Этап 1: Компиляция компилятора YaoXiang на Rust                        │
│                                                                           │
│  JSON упаковывается в бинарный файл компилятора                         │
│  Цель: команда explain может напрямую читать данные i18n                  │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│  Этап 2: Компиляция пользовательского проекта на YaoXiang (рендеринг здесь)│
│                                                                           │
│  При вызове макроса error!:                                              │
│  1. Чтение yaoxiang.toml для получения языковых предпочтений             │
│  2. Загрузка i18n JSON для соответствующего языка из бинарного файла     │
│  3. template + params → render() → "Unknown variable: 'x'"              │
│  4. Diagnostic.message = отрендеренная строка                           │
│                                                                           │
│  AOT бинарный файл напрямую хранит финальную строку, без шаблонов,       │
│  без поиска по таблицам                                                   │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│  Этап 3: Время выполнения пользовательской программы                    │
│                                                                           │
│  println!("{}", diagnostic.message)                                     │
│  // Прямой вывод финальной строки, без какого-либо поиска по таблицам     │
└─────────────────────────────────────────────────────────────────────────┘
КомпонентОтветственностьВремя рендеринга
I18nRegistryПредоставление шаблонов и отображаемого текстаПри компиляции пользовательского проекта
DiagnosticBuilder.render()Шаблон + параметры → финальная строкаПри компиляции пользовательского проекта
Diagnostic.messageОтрендеренная строкаХранение финального результата
AOT бинарный файлСодержит финальные строкиПрямое использование во время выполнения

Формат сообщений об ошибках

Сообщения об ошибках используют следующий формат:

error[E####]: <краткое описание>
  --> <файл>:<строка>:<столбец>
   <строка> | <фрагмент кода>
          ^^^<подсветка>

Полный пример

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

Уровни серьёзности

Серьёзность ошибки управляется через перечисление DiagnosticLevel, независимо от нумерации кодов ошибок:

rust
pub enum DiagnosticLevel {
    Error,    // Приводит к сбою компиляции
    Warning,  // Не влияет на компиляцию, но рекомендуется исправить
    Note,     // Дополнительная информация
    Help,     // Предложение по исправлению
}
УровеньПрефиксОписание
Errorerror[E####]:Приводит к сбою компиляции
Warningwarning[E####]:Не влияет на компиляцию
Notenote[E####]:Дополнительная информация
Helphelp[E####]:Предложение по исправлению

Команда yaoxiang explain

Синтаксис команды

bash
yaoxiang explain <ERROR_CODE> [OPTIONS]

Опции

ОпцияОписание
--lang <code>Указать язык (en-US, ru-RU, zh-CN, по умолчанию en-US)
--jsonВывод в формате JSON (для IDE/LSP)
--json-prettyФорматированный вывод JSON
--examplesПоказывать только примеры кода
--helpПоказать справочную информацию

Примеры использования

bash
# По умолчанию на английском
$ yaoxiang explain E1001
error[E1001]: Unknown variable: {name}
  --> <file>:<line>:<col>

Help: Did you mean to define it?

Example:
  let {name} = value;

# Вывод на русском
$ yaoxiang explain E1001 --lang ru
error[E1001]: Неизвестная переменная: {name}
  --> <file>:<line>:<col>

Справка: Возможно, вы хотели её определить?

Пример:
  let {name} = value;

# JSON вывод (интеграция LSP)
$ 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 вывода

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

Обратная совместимость

Поскольку настоящий RFC проектирует систему кодов ошибок с нуля, проблемы обратной совместимости не существует.

Стратегия миграции в будущем (для справки в последующих версиях):

  1. Сохранить отображение старых кодов ошибок в новые
  2. В период миграции отображать одновременно старые и новые коды
  3. Предоставить график устаревания

Стратегия реализации

Этап первый: Базовая инфраструктура кодов ошибок

  1. Создать структуру каталогов src/diagnostics/
  2. Реализовать перечисление ErrorCode
  3. Реализовать Diagnostic и DiagnosticLevel
  4. Создать каталог файлов ресурсов и примеры JSON

Этап второй: Команда explain

  1. Реализовать CLI команду yaoxiang explain
  2. Поддержать опции --lang и --json
  3. Интегрировать загрузку файлов ресурсов
  4. Реализовать рендеринг параметров шаблона

Этап третий: Интеграция с компилятором

  1. Обновить все точки отчёта об ошибках для использования новой системы
  2. Реализовать внедрение параметров шаблона сообщений
  3. Добавить логику приоритета языков
  4. Покрыть юнит-тестами

Этап четвёртый: Интеграция с IDE/LSP

  1. Интегрировать JSON вывод explain в LSP сервер
  2. Отображать ссылки на коды ошибок в IDE
  3. Показывать объяснения ошибок при наведении
  4. Предложения по быстрому исправлению

Приложения

Полная шпаргалка по кодам ошибок

ДиапазонКатегория
E0xxxЛексический и синтаксический анализ
E1xxxПроверка типов
E2xxxСемантический анализ
E3xxxГенерация кода
E4xxxОбобщения и trait-ы
E5xxxМодули и импорт
E6xxxОшибки времени выполнения
E7xxxОшибки ввода-вывода и системы
E8xxxВнутренние ошибки компилятора
E9xxxЗарезервировано

Поддерживаемые языки

КодЯзыкСтатус
en-USEnglish (US)По умолчанию
ru-RUРусскийЗапланировано
zh-CNКитайский (упрощённый)Запланировано

Сравнение примеров сообщений об ошибках

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

# Russian (ru-RU)
error[E1001]: Неизвестная переменная: x
  --> src/main.yx:5:12
   5 |   print(x)
          ^
          справка: Возможно, вы хотели её определить?

Список литературы