RFC 013: Спецификация кодов ошибок
Резюме
Настоящий RFC предлагает спецификацию классификации кодов ошибок компилятора YaoXiang, использующую одноуровневую систему нумерации по аналогии с Rust, в сочетании с файлами ресурсов JSON для поддержки мультиязычности, с предоставлением объяснений ошибок через команду yaoxiang explain.
Мотивация
Зачем нужна стандартизированная система кодов ошибок?
- Пользовательский опыт: пользователь, видя код ошибки, может быстро определить её тип и серьёзность
- Организация документации: группировка по категориям упрощает написание и поддержку справочной документации по ошибкам
- Интеграция с инструментами: IDE/LSP могут предлагать быстрые исправления и ссылки на документацию на основе кода ошибки
- Поддержка интернационализации: разделение сообщений об ошибках и кодов упрощает мультиязычный перевод
Цели проектирования
- Простота: одноуровневая нумерация, пользователю не нужно запоминать сложные правила классификации
- Дружелюбность: формат сообщений об ошибках, аналогичный Rust, с подсказками и примерами
- Расширяемость: управление через файлы ресурсов, лёгкость добавления новых ошибок и языков
- Удобство для инструментов: команда explain + вывод в формате JSON, поддержка интеграции с IDE/LSP
Предложение
Основной дизайн: одноуровневая система нумерации
Используется четырёхзначная нумерация с группировкой по фазам компиляции:
Exxxx
││││
│││└── Порядковый номер (000-999)
││└─── Фаза компиляции (0-9)
└───── Фиксированный префикс 'E'Разделение по фазам
| Фаза | Диапазон | Описание |
|---|---|---|
| 0 | E0xxx | Лексический и синтаксический анализ |
| 1 | E1xxx | Проверка типов |
| 2 | E2xxx | Семантический анализ |
| 3 | E3xxx | Генерация кода |
| 4 | E4xxx | Дженерики и трейты |
| 5 | E5xxx | Модули и импорт |
| 6 | E6xxx | Ошибки времени выполнения |
| 7 | E7xxx | I/O и системные ошибки |
| 8 | E8xxx | Внутренние ошибки компилятора |
| 9 | E9xxx | Зарезервировано/экспериментальное |
Перечисление категорий ошибок
/// Категория ошибки
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
Lexer, // E0xxx: Лексический и синтаксический анализ
Parser, // E0xxx: Parser errors
TypeCheck, // E1xxx: Проверка типов
Semantic, // E2xxx: Семантический анализ
Generic, // E4xxx: Дженерики и трейты
Module, // E5xxx: Модули и импорт
Runtime, // E6xxx: Ошибки времени выполнения
Io, // E7xxx: I/O и системные ошибки
Internal, // E8xxx: Внутренние ошибки компилятора
}Определение кода ошибки и общий Builder
Основной принцип: разделение определения кода ошибки и отображаемого текста
ErrorCodeDefinition: метаданные кода ошибки (code, category, template), без отображаемого текстаlocales/*.json: отображаемый текст на различных языках (title, message, help, коды ошибок в виде вложенных объектов)DiagnosticBuilder: универсальный построитель, заменяющий дизайн с trait-per-error
Определение кода ошибки
// 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 (рендеринг шаблона выполняется в compile-time)
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(),
}
}
}Сокращённые методы для каждого кода ошибки
// 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)
}
}Пример использования
// 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));Пример определения кода ошибки
// 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 |
| Нулевые накладные расходы в runtime | Рендеринг в compile-time, AOT-бинарник без таблиц поиска |
Упрощение с помощью макросов ошибок
Макрос error! (автоматическая инъекция контекста)
/// Макрос для автоматического получения span и конфигурации i18n в 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())
};
}
/// Использование: нужно передать только параметры, span и i18n инжектируются автоматически
return Err(error!(E1001, name = var_name));
return Err(error!(E1002, expected = "bool", found = cond_ty));Ручное использование Builder
// Когда нужен ручной контроль
E1001::unknown_variable(&var_name)
.at(my_span) // Пользовательский span
.build(&custom_i18n) // Пользовательский i18nДетальный дизайн
Список кодов ошибок
E0xxx: Лексический и синтаксический анализ
| Код | Описание |
|---|---|
| E0001 | Недопустимый символ |
| E0002 | Недопустимый числовой литерал |
| E0003 | Незакрытая строка |
| E0004 | Недопустимый символьный литерал |
| E0010 | Ожидаемый токен |
| E0011 | Неожиданный токен |
| E0012 | Недопустимый синтаксис |
| E0013 | Несоответствующая скобка |
| E0014 | Отсутствует точка с запятой |
| E0016 | Ожидаемое выражение |
| E0018 | Ключевое слово в качестве имени |
E1xxx: Проверка типов
| Код | Описание |
|---|---|
| E1001 | Неизвестная переменная |
| E1002 | Несоответствие типов |
| E1003 | Неизвестный тип |
| E1010 | Несоответствие количества аргументов |
| E1011 | Несоответствие типов аргументов |
| E1012 | Несоответствие возвращаемого типа |
| E1013 | Функция не найдена |
| E1014 | Неизвестное имя именованного аргумента |
| E1015 | Повторное указание аргумента |
| E1020 | Невозможно вывести тип |
| E1021 | Конфликт вывода типов |
| E1030 | Неполный шаблон |
| E1031 | Недостижимый шаблон |
| E1040 | Операция не поддерживается |
| E1041 | Выход за границы индекса |
| E1042 | Поле не найдено |
| E1050 | Требуется логический операнд |
| E1051 | Логическое NOT требует логического операнда |
| E1052 | Недопустимое разыменование |
| E1053 | Доступ к полю неструктурного типа |
| E1054 | Несоответствие условного типа |
| E1055 | Ограничение в не-generic контексте |
| E1060 | Несоответствие количества типовых параметров |
| E1061 | Невозможно инстанцировать дженерик |
| E1062 | Ограничение const-дженерика не выполнено |
| E1064 | Недопустимый индекс позиции привязки |
| E1065 | Вызов не-функционального значения |
| E1071 | Определение типа допустимо только на уровне модуля |
| E1081 | ? допустим только в функциях, возвращающих Result |
| E1082 | ? может использоваться только для выражений Result |
| E1083 | Несоответствие типа ошибки для ? |
| E1090 | ✨ Несказуемое ✨ |
| E1091 | Недопустимый мета-тип дженерика |
| E1092 | Недопустимая форма аргумента уточнённого типа |
| E1093 | Несоответствие количества уточнённых аргументов |
| E1094 | Неиспользованный параметр compile-time значения |
| E1095 | Неизвестный интерфейс |
| E1096 | Несоответствие количества параметров интерфейса |
| E1097 | Конфликт имён членов интерфейса |
| E1098 | Метод интерфейса не реализован |
| E1099 | Несоответствие сигнатуры метода интерфейса |
| E1100 | Повторная реализация метода интерфейса |
| E1101 | Тип не реализует интерфейс |
| E1102 | Управляющий оператор цикла вне цикла |
| E1103 | Квадратные скобки недопустимы в позиции типа |
| E1104 | Реализация интерфейса вне модуля определения типа |
| E1105 | Конструктор варианта не может использоваться как доступ к полю |
Связь с RFC-011b (примечание от 2026-09-22): RFC-011b: Перегрузка операторов При реализации будут затронуты три места в данном разделе — ① тексты
E1081/E1082освободятся от слова "Result" (?будет определяться через интерфейсTry, без привязки к конкретному имени типа), синхронизация по процессу «трёхсторонней согласованности» данного документа (codes/*.rs ↔ locales ↔ таблица кодов) в фазе 2; ② невыполнение предварительного ограниченияEqual(линейный токен) будет использовать семействоE1101(тип не реализует интерфейс) для отказа; ③ после подключения фазы 1Struct == Structпреобразуется из ошибки runtimeE6007в compile-time проверку, поверхность срабатыванияE6007сократится. Зарегистрированные тексты в таблице остаются в текущем виде до завершения реализации.
E2xxx: Семантический анализ
| Код | Описание |
|---|---|
| E2001 | Ошибка области видимости |
| E2002 | Повторное определение |
| E2003 | Ошибка владения |
| E2010 | Присваивание неизменяемому |
| E2011 | Использование неинициализированной переменной |
| E2012 | Конфликт изменяемости |
| E2013 | Затенение переменной |
| E2014 | Использование перемещённого значения |
| E2016 | Присваивание неизменяемому |
| E2018 | Конфликт изменяемого/неизменяемого заимствования |
| E2019 | Двойное освобождение |
| E2020 | Использование после освобождения |
| E2027 | Разыменование unsafe |
| E2029 | Циклическая ссылка в spawn |
| E2030 | Нарушение ограничения уточнённого типа |
| E2090 | Недопустимая сигнатура |
| E2091 | Неизвестный тип в сигнатуре |
| E2092 | В сигнатуре отсутствует стрелка |
| E2093 | Повторяющееся имя параметра |
| E2094 | Затенение параметра дженерика |
| E2095 | Затенение имени параметром имени дженерика |
Пояснение о зарезервированных кодах (инвентаризация от 2026-09-14, релизная позиция #251): коды E2019 (двойное освобождение), E2020 (использование после освобождения), E2027 (разыменование unsafe), E2029 (циклическая ссылка в spawn) зарегистрированы и закреплены модульными тестами, но пока не имеют достижимого поверхностного слоя в исходном коде yx (явный оператор drop, синтаксис разыменования Ptr, пути построения циклов ref в spawn) — утверждение о полной семантической корректности не распространяется на эти четыре кода, до завершения реализации они считаются «зарезервированными».
E3xxx: Генерация кода
| Код | Описание |
|---|---|
| E3004 | Неподдерживаемый итератор |
| E3005 | Ошибка генерации IR |
| E3006 | Неразрешённая переменная |
| E3007 | Инициализация привязки верхнего уровня должна быть константой |
| E3008 | Неподдерживаемый шаблон match |
| E3014 | Переполнение регистров |
| E3017 | Недопустимый операнд (генерация кода) |
| E3018 | Сбой инстанцирования при мономорфизации |
| E3019 | Циклическая зависимость привязок верхнего уровня |
| E3020 | Отсутствует точка входа программы |
| E3021 | Точка входа не является функцией |
| E3022 | Сигнатура точки входа main не соответствует |
| E3023 | Исполняемые операторы недопустимы на верхнем уровне |
E4xxx: Дженерики и трейты
| Код | Описание |
|---|---|
| E4001 | Нарушение ограничения дженерика |
| E4002 | Трейт не найден |
| E4003 | Реализация трейта отсутствует |
| E4004 | Конфликт реализаций трейта |
| E4005 | Ассоциированный тип не найден |
| E4010 | Деление константы на ноль |
| E4011 | Переполнение константы |
| E4012 | Слишком глубокая рекурсия констант |
| E4014 | Сбой вычисления константы |
| E4018 | Нарушение уточняющего предиката |
| E4019 | Равенство типов не выполняется |
| E4020 | Требуется функция-доказательство |
E4006/E8004 в настоящее время не имеют точек срабатывания (зарезервированные коды): ограничение Sized и пути ошибок оптимизации ожидают реализации, при реализации будут подключены к реальным поверхностям срабатывания.
E5xxx: Модули и импорт
| Код | Описание |
|---|---|
| E5001 | Модуль не найден |
| E5002 | Ошибка импорта |
| E5003 | Экспорт не найден |
| E5004 | Циклическая зависимость |
| E5005 | Недопустимый путь модуля |
| E5006 | Повторный импорт |
| E5007 | Экспорт модуля |
E6xxx: Ошибки времени выполнения
| Код | Описание |
|---|---|
| E6001 | Деление на ноль |
| E6003 | Выход индекса массива за границы |
| E6004 | Переполнение стека |
| E6005 | Сбой утверждения |
| E6006 | Функция не найдена (runtime) |
| E6007 | Ошибка времени выполнения |
| E6008 | Ключ не существует |
| E6009 | Недопустимый шаг Range |
| E6010 | Сбой парсинга целого числа |
| E6011 | Сбой парсинга числа с плавающей точкой |
Редакция таблицы кодов (2026-08-09): исходная таблица кодов была определена по семантическому проекту Rust (Assertion failed/Arithmetic overflow/Heap allocation failed/Type cast failed), что не соответствует реальным потребностям реализации. В YaoXiang отсутствуют понятия нулевого указателя/сбоя кучи/приведения типов (семантика значений + безопасность памяти Rust), путь runtime-переполнения не реализует обнаружение. После корректировки:
- E6002 удалён (исходный Assertion failed перемещён в E6005; семантика нулевого указателя не имеет аналога в языке)
- E6003 изменён с Arithmetic overflow на Runtime index out of bounds (реальная поверхность срабатывания)
- E6005 изменён с Heap allocation failed на Assertion failed (реальный путь std.assert)
- E6006 изменён с Runtime index out of bounds на Function not found (так было в реализации изначально)
- E6007 изменён с Type cast failed на общий Runtime error (унифицированная точка для неотображённых вариантов ExecutorError)
E7xxx: I/O и системные ошибки
| Код | Описание |
|---|---|
| E7001 | Файл не найден |
| E7002 | Доступ запрещён |
| E7003 | Ошибка I/O |
| E7004 | Сетевая ошибка |
E8xxx: Внутренние ошибки компилятора
| Код | Описание |
|---|---|
| E8001 | Внутренняя ошибка компилятора |
| E8002 | Неожиданный Panic |
| E8003 | Ошибка фазы компилятора |
W1xxx: Коды предупреждений
| Код | Описание |
|---|---|
| W1001 | Неиспользуемая приватная функция |
| W1002 | Неиспользуемый приватный тип |
| W1003 | Неиспользуемый импорт |
| W1004 | Неиспользуемая приватная переменная |
| W1005 | Неиспользуемый приватный метод |
| W1063 | Невозможно вычислить ограничение const-дженерика |
| W1080 | Деградация compile-time доказательства |
Правило распределения W-кодов: изоморфно E-кодам, группировка по фазам (W + фаза в тысячных разрядах), W1xxx = предупреждения фазы проверки типов.
Семантика кода мёртвого кода (решение B по #321): определения с
pubявляются внешним интерфейсом и никогда не репортируются — вопрос использования внешним потребителем выходит за границы анализа одного файла, лучше молчать, чем ложно срабатывать. W1001/W1002/W1004/W1005 применяются только к приватным (неpub) определениям: если в анализе достижимости, начиная сmainиpubопределений, определение никогда не используется — репортируется. Для методов (W1005) сопоставление по короткому имени в точке вызова. Семантика bin/lib target (репорт неиспользуемого pub в bin) — будущее расширение (вариант A по #289), требует поддержки со стороны модели проекта.Неиспользуемый импорт (W1003): обнаруживается на этапе use-elaboration в typecheck (pass2 регистрирует локальные имена импорта; попадание в выражения и позиции аннотаций типов считается использованием), покрывает как глобальный импорт (
use std.io→ псевдоним модуля), так и именованный (use std.io.{print}).Канал срабатывания: W-диагностики от builder по умолчанию помечаются префиксом W как
Severity::Warning(явное указание имеет приоритет), сбор и отображение идут по тому же пути, что и ошибки (рендеринг с префиксомwarning[W####]), но не блокируют компиляцию и не влияют на код успешного завершения.yaoxiang check --deny-warningsповышает предупреждения до ошибки (ненулевой код выхода при наличии предупреждений), для строгого режима CI. Подавление per-code (атрибуты allow и т.п.) — пункт для последующего расширения.
Спецификация качества сообщений
Данный раздел введён ревизией унификации и качества сообщений (2026-09-03). Принудительно выполняется в CI скриптом
scripts/audit_diagnostics.py.
- Единый путь сообщений: все видимые пользователю диагностические сообщения должны проходить через сокращённые методы авторитетного реестра + рендеринг шаблонов locales, код передаёт только структурированные параметры. Запрещено обходить реестр и напрямую конструировать сырые значения вроде
Diagnostic::error(...)— этот путь обходит проверку кодов и i18n. - Легитимность кода: запрещено использовать незарегистрированные коды и псевдокоды (например,
E_INTERNAL); литералы кода в точке использования должны быть определены в реестре. Внутренние ошибки всегда приводят к E8001 (internal_error). - Отображение типов: Display типов должен различать форму до и после инстанцирования (голое имя
Expected 'Container', found 'Container'неразличимо). - Изоляция внутреннего состояния солвера: промежуточные TypeVar солвера (формат Display
t<N>) не должны попадать в видимые пользователю сообщения. Тестовый якорь:test_type_error_message_no_solver_typevar_leak. - Граница E8xxx: E8xxx используется только для внутренних проблем согласованности компилятора (ICE). Ошибки, которые пользователь может исправить, запрещено маскировать под E8001; сообщения ICE должны сопровождаться указаниями по минимальному воспроизведению.
Значения ошибок runtime и сквозное использование кодов
Данный раздел введён ревизией привязки кода к значениям ошибок runtime (2026-09-03). Семантическое пространство E6xxx/E7xxx одновременно обслуживает два канала, пространство кодов едино, каналы отображения различны.
Два канала
| Канал | Носитель | Способ отображения |
|---|---|---|
| Канал диагностики компилятора/CLI | ExecutorError и другие жёсткие ошибки уровня хоста | stderr error[E####]: (уже подключены E6003/E6005/E6007) |
| Канал значений ошибок внутри программы | Err-носитель Error типа Result(T, Error) из std | Значение языка, потребляется программой через match/сравнение |
Структура Error (с v0.8, ломающее изменение)
Error { code: String, message: String }codeповторно использует нумерацию E6xxx/E7xxx данной спецификации, в строковом виде (например,"E6008").- Стабильный контракт: выделенные коды сохраняют семантику между версиями; одна и та же семантика не переиспользует удалённый код (прецедент E6002).
- Поверхность потребления: сравнение
e.code == "E6xxx"внутри программы — единственный программируемый контракт определения; документацияyaoxiang explain E6xxxсквозная; инструментарий (LSP / DAP, см. RFC-034) использует код как exceptionId. - Аксессоры:
std.result.code(e)/std.result.message(e). - Пользовательские ошибки:
EвResult(T, E)— параметр дженерика; для серьёзного моделирования используются пользовательские типы; stdError— лишь удобный резервный носитель, его система кодов не ограничивает пользовательские типы E.
Правила распределения кодов
- Коды значений ошибок runtime и диагностические коды компилятора разделяют пространство E6xxx/E7xxx; новые коды выделяются по реальной поверхности срабатывания, без резервирования под воображаемые сценарии.
- Сначала регистрация, затем использование: новый код должен быть внесён в авторитетный реестр и пройти проверку трёхсторонней согласованности (codes/*.rs ↔ locales ↔ таблица кодов данного документа), прежде чем он может быть отправлен. Источником регистрации кодов значений ошибок runtime служит таблица
RUNTIME_ERROR_CODESвsrc/std/result.rs(наряду с диагностическими кодами проходит порог build-time вbuild.rs+ проверкуtools/code-tables). - E7xxx — зарезервированный сегмент для значений ошибок std.io / std.net (в данный момент пуст, будет активирован при превращении io/net в Result).
- Точки срабатывания: модули std конструируют значение Error через
error_new(code, message); на стороне потребителяstd.result.unwrap_errизвлекает Err-носитель,std.result.code/messageчитают поля.
Путь эволюции (линия C, не реализовано)
После завершения полноты сопоставления с образцом (RFC-010b) Error может быть обновлён до { kind: ErrorKind, message: String }, где code становится свойством, выводимым из kind (определение варианта — точка регистрации кода). В период эволюции стабильный контракт кода из данного раздела остаётся неизменным; это обновление — отдельное решение, не является обязательством данного раздела.
Файлы ресурсов для мультиязычности
Формат файла ресурсов
// locales/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'"
}
}// locales/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
// locales/*.json (объекты кодов ошибок)
/// Реестр отображаемых текстов i18n (загружается в compile-time из JSON, нулевые таблицы поиска в runtime)
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 {
"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(),
})
}
/// Рендеринг шаблона (выполняется в compile-time, нулевые накладные расходы в 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
}
}Плейсхолдеры шаблонов
Предопределённые плейсхолдеры (часто используемые)
| Плейсхолдер | Назначение | Пример |
|---|---|---|
{name} | Имя переменной/типа/трейта и т.п. | Unknown variable: '{name}' |
{expected} | Ожидаемый тип | Expected type '{expected}' |
{found} | Фактический/найденный тип | , found type '{found}' |
{method} | Имя метода | Method {method} is not a function |
{trait} | Имя трейта | Cannot find trait: {trait} |
{path} | Путь модуля | Invalid path: {path} |
{ty} | Выражение типа | Invalid type: {ty} |
{message} | Сообщение внутренней ошибки | Internal error: {message} |
Поддержка произвольных ключей
params поддерживает произвольные ключи, не ограничиваясь предопределёнными. Вызывающий может передать любой key:
// Использование произвольного ключа
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
Конфигурация уровня проекта
# yaoxiang.toml
[project]
name = "my-project"
version = "0.1.0"
[language]
# Язык сообщений об ошибках, возможные значения: en, zh, ja, ...
default = "zh"Конфигурация уровня пользователя
# ~/.yaoxiang/yaoxiang.toml
[language]
default = "zh"Выбор языка в compile-time
1. Чтение language.default из yaoxiang.toml уровня проекта
2. Если не настроено, чтение ~/.yaoxiang/yaoxiang.toml уровня пользователя
3. Если оба не настроены, по умолчанию используется "en"
4. Компилятор создаёт I18nRegistry согласно выбранному языку (однократно)
5. Все ошибки используют этот I18nRegistry для рендеринга сообщенийКлюч к нулевым накладным расходам поиска
Рендеринг происходит при компиляции пользовательского проекта, а не в runtime.
┌─────────────────────────────────────────────────────────────────────────┐
│ Фаза 1: Rust-компиляция компилятора YaoXiang │
│ │
│ JSON упаковывается в бинарник компилятора │
│ Цель: команда explain может напрямую читать данные i18n │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Фаза 2: YaoXiang компилирует пользовательский проект (здесь происходит │
│ рендеринг) │
│ │
│ При вызове макроса error!: │
│ 1. Чтение yaoxiang.toml для получения языковых предпочтений │
│ 2. Загрузка JSON нужного языка из бинарника компилятора │
│ 3. Шаблон + параметры → render() → "Unknown variable: 'x'" │
│ 4. Diagnostic.message = отрендеренная строка │
│ │
│ AOT-бинарник напрямую хранит итоговую строку, без шаблонов и поиска │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Фаза 3: Выполнение пользовательской программы │
│ │
│ println!("{}", diagnostic.message) │
│ // Прямой вывод итоговой строки, без какого-либо поиска │
└─────────────────────────────────────────────────────────────────────────┘| Компонент | Обязанности | Момент рендеринга |
|---|---|---|
I18nRegistry | Предоставление шаблонов и отображаемых текстов | При компиляции пользовательского проекта |
DiagnosticBuilder.render() | Шаблон + параметры → итоговая строка | При компиляции пользовательского проекта |
Diagnostic.message | Отрендеренная строка | Хранит итоговый результат |
| AOT-бинарник | Содержит итоговые строки | Используется напрямую в runtime |
Формат сообщений об ошибках
Сообщения об ошибках используют следующий формат:
error[E####]: <краткое описание>
--> <файл>:<строка>:<столбец>
<строка> | <фрагмент кода>
^^^<подсветка>Полный пример
error[E1001]: Unknown variable: x
--> src/main.yx:5:12
5 | print(x)
^
help: Did you mean to define it?Уровни серьёзности
Уровень серьёзности ошибки управляется перечислением DiagnosticLevel, декаплированным от номера кода ошибки:
pub enum DiagnosticLevel {
Error, // Приводит к сбою компиляции
Warning, // Не влияет на компиляцию, но рекомендуется исправить
Note, // Дополнительная информация
Help, // Предложение по исправлению
}| Уровень | Префикс | Описание |
|---|---|---|
| Error | error[E####]: | Приводит к сбою компиляции |
| Warning | warning[E####]: | Не влияет на компиляцию |
| Note | note[E####]: | Дополнительная информация |
| Help | help[E####]: | Предложение по исправлению |
Команда yaoxiang explain
Синтаксис команды
yaoxiang explain <ERROR_CODE> [OPTIONS]Опции
| Опция | Описание |
|---|---|
--lang <code> | Указать язык (en-US, zh-CN, по умолчанию en-US) |
--json | Вывод в формате JSON (для IDE/LSP) |
--json-pretty | Форматированный JSON-вывод |
--examples | Показывать только примеры кода |
--help | Показать справку |
Примеры использования
# По умолчанию на английском
$ 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 zh
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-вывода
{
"code": "E1001",
"message": "Unknown variable: {name}",
"help": "Did you mean to define it?",
"examples": ["let {name} = value;"],
"language": "en-US"
}Обратная совместимость
Поскольку данный RFC проектирует систему кодов ошибок с нуля, проблемы обратной совместимости отсутствуют.
Стратегия будущей миграции (для справки в последующих версиях):
- Сохранять отображение старых кодов ошибок на новые
- В период миграции одновременно отображать старые и новые коды
- Предоставить график устаревания
Стратегия реализации
Фаза первая: базовая инфраструктура кодов ошибок
- Создать структуру каталога
src/diagnostics/ - Реализовать перечисление
ErrorCode - Реализовать
DiagnosticиDiagnosticLevel - Создать каталог файлов ресурсов и примеры JSON
Фаза вторая: команда explain
- Реализовать CLI-команду
yaoxiang explain - Поддержка опций
--langи--json - Интеграция загрузки файлов ресурсов
- Реализация рендеринга шаблонов с параметрами
Фаза третья: интеграция в compile-time
- Обновить все точки репортинга ошибок для использования новой системы
- Реализовать инъекцию параметров шаблона сообщения
- Добавить логику приоритета языка
- Покрытие модульными тестами
Фаза четвёртая: интеграция с IDE/LSP
- LSP-сервер интегрирует JSON-вывод команды explain
- Отображение ссылок на коды ошибок в IDE
- Показ объяснения ошибки при наведении
- Предложения быстрых исправлений
Приложение
Полная сводная таблица кодов ошибок
| Диапазон | Категория |
|---|---|
| E0xxx | Лексический и синтаксический анализ |
| E1xxx | Проверка типов |
| E2xxx | Семантический анализ |
| E3xxx | Генерация кода |
| E4xxx | Дженерики и трейты |
| E5xxx | Модули и импорт |
| E6xxx | Ошибки времени выполнения |
| E7xxx | I/O и системные ошибки |
| E8xxx | Внутренние ошибки компилятора |
| E9xxx | Зарезервировано |
Поддерживаемые языки
| Код | Язык | Статус |
|---|---|---|
| en-US | English (US) | По умолчанию |
| zh-CN | 简体中文 | В планах |
Сравнение примеров сообщений об ошибках
# Английский (en-US)
error[E1001]: Unknown variable: x
--> src/main.yx:5:12
5 | print(x)
^
help: Did you mean to define it?
# Китайский (zh-CN)
error[E1001]: 未知变量: x
--> src/main.yx:5:12
5 | print(x)
^
帮助: 你是否想要定义它?