Компиляторы
Семантические ошибки и диагностика
Elm когда-то был известен как 'язык с лучшими сообщениями об ошибках в мире'. Его создатель Evan Czaplicki написал в блоге: 'Compiler errors are user interface'. Это изменило то, как индустрия смотрит на диагностику - Rust, Swift, Kotlin переняли подход. Хорошая ошибка компилятора экономит часы отладки.
- **Rustc** включает более 600 уникальных кодов ошибок (E0001-E0799). Каждый имеет развёрнутое объяснение: `rustc --explain E0502` выводит многостраничный документ с примерами и объяснением borrow checker.
- **TypeScript Language Server** использует структурированную диагностику для встроенных подсказок в VS Code - те самые красные подчёркивания с tooltip работают через тот же JSON-формат, что и `tsc --strict`.
- **Clang** поддерживает `-fdiagnostics-format=sarif` с версии 16, что позволяет GitHub Actions напрямую показывать предупреждения компилятора как комментарии к PR без дополнительных скриптов.
Error Messages
Качество сообщений об ошибках - одна из самых важных пользовательских характеристик компилятора. Ранние компиляторы C давали просто `syntax error` без указания строки. Современные компиляторы - Rust, Elm, Clang - дают детальные объяснения с указанием причины, контекста и способа исправления.
Rust team инвестировала значительные усилия в error messages: каждый тип ошибки имеет уникальный код (E0502), объяснение доступно через `rustc --explain E0502`. В 2016 году команда проводила user studies, сравнивая Rust с Haskell и C++ по понятности ошибок - это привело к переписыванию значительной части диагностики.
Что из перечисленного наиболее важно для качественного сообщения об ошибке компилятора?
Error Recovery
Error recovery - способность компилятора продолжать анализ после нахождения первой ошибки, чтобы сообщить о как можно большем числе проблем за один запуск. Без recovery компилятор останавливается на первой ошибке - пользователь вынужден запускать его многократно.
TypeScript compiler в режиме '--noEmitOnError false' продолжает генерировать JS даже при наличии ошибок - это критично для IDE: language server должен давать автодополнение даже в частично сломанном коде. Roslyn (C# compiler) построен на принципе 'всегда строить полное синтаксическое дерево' - даже с ошибками, вставляя специальные ErrorNodes в AST.
Зачем компилятор использует специальный тип `ErrorType` при type checking?
Source Spans
Source span - диапазон позиций в исходном коде (файл, строка, колонка начала и конца). Каждый узел AST несёт свой span. Это позволяет компилятору точно указать, какой именно фрагмент кода вызвал ошибку.
Rust compiler хранит все spans в интернированном `SourceMap` - одна глобальная структура данных вместо строк в каждом узле. Это экономит память: вместо полного пути к файлу каждый span хранит только 8 байт (lo + hi). При выводе ошибки компилятор обращается к SourceMap один раз для восстановления полного контекста.
Почему Rustc хранит spans как (BytePos, BytePos) вместо (file, line, col)?
Diagnostics Infrastructure
Diagnostics infrastructure - система формирования и вывода сообщений об ошибках. Современные компиляторы строят диагностику как структурированный объект (не строку), который затем рендерится в разные форматы: человекочитаемый текст, JSON для IDE, SARIF для CI-систем.
SARIF (Static Analysis Results Interchange Format) - JSON-стандарт для диагностики от компиляторов и линтеров. GitHub Code Scanning, Azure DevOps и другие CI-платформы понимают SARIF - компилятор или линтер может напрямую создавать аннотации к PR. Clang, cppcheck, ESLint умеют выводить SARIF. Это позволяет встраивать статический анализ в code review без дополнительных инструментов.
Зачем компилятор строит диагностику как структурированный объект, а не просто форматирует строку сразу?
Ключевые идеи
- **Качество error messages** - измеримая характеристика компилятора. Rustc, Elm, Kotlin инвестировали в это значительно, что повлияло на adoption языков.
- **Error recovery** позволяет находить несколько ошибок за один запуск. Sentinel `ErrorType` предотвращает cascade errors при type checking.
- **Source spans** хранятся компактно (byte offsets) в каждом узле AST и используются для точного указания места ошибки.
- **Structured diagnostics** разделяют построение и вывод ошибки - один компилятор может рендерить в текст, JSON (для IDE) и SARIF (для CI).
Связанные темы
Диагностика пронизывает все фазы компилятора - от парсинга до code generation:
- Проверка типов — Type mismatch - самый частый источник семантических ошибок, требующих качественной диагностики
- Таблица символов — Ошибки undefined variable и redeclaration обнаруживаются при построении symbol table
- Семантические проходы — Диагностика собирается на всех семантических проходах и выводится в конце фазы
Вопросы для размышления
- Почему компиляторы C исторически давали плохие сообщения об ошибках - это технические ограничения или культурные нормы эпохи?
- Как error recovery в IDE (real-time re-parse на каждое нажатие клавиши) отличается от batch-компиляции - какие дополнительные требования это накладывает?
- SARIF позволяет статическому анализатору создавать аннотации прямо в GitHub PR. Какие новые возможности это открывает для автоматизации code review?