Компиляторы
Language Server Protocol
VS Code стал стандартом для разработки на 20+ языках. Секрет - Language Server Protocol: один Rust language server (rust-analyzer) работает в VS Code, Neovim, Emacs, Helix. Один Go server (gopls) во всех редакторах. Это как HTTP для интернета - общий протокол освободил разработчиков языков от необходимости поддерживать плагины для каждого редактора.
- **rust-analyzer**: обрабатывает Rust workspace в 2-5 секунд и затем отвечает на completions за 20-100ms; написан с нуля специально для IDE (не использует rustc frontend)
- **clangd в Google**: используется для code navigation в монорепо с миллиардами строк C++ кода - поддерживает remote index для предварительно построенных symbols без локального индексирования
- **GitHub code navigation**: использует Tree-sitter для go-to-definition и find-references прямо на github.com для 50+ языков без запуска полного компилятора
Language Server Protocol
LSP (Language Server Protocol) - JSON-RPC протокол между редактором (клиент) и language server (сервер с компиляторным frontend). Придуман Microsoft для VS Code в 2016, сейчас стандарт де-факто. Language server запускается как отдельный процесс: получает уведомления об изменениях файлов, отвечает на запросы completions/hover/definitions.
До LSP: каждый редактор писал отдельный плагин для каждого языка = N*M комбинаций. С LSP: 1 language server * N редакторов. rust-analyzer (Rust LSP) поддерживается VS Code, Neovim, Emacs, Helix, Zed. clangd (C++) встроен в CLion, VS Code, Qt Creator. Pylsp/Pyright - Python. gopls - Go. Все используют один протокол.
Какую проблему решил LSP в экосистеме редакторов?
Инкрементальный парсинг
IDE не может перепарсивать файл целиком при каждом нажатии клавиши. Инкрементальный парсер пересчитывает только затронутую часть дерева. Tree-sitter - популярный инкрементальный парсер: хранит конкретное синтаксическое дерево (CST), при изменении текста пересчитывает минимальный поддерево.
GitHub использует Tree-sitter для code navigation (go-to-def на github.com) и syntax highlighting для 50+ языков. Neovim 0.5+ интегрировал Tree-sitter как стандартный парсер заменив regex-based подсветку. Incremental computation framework Salsa (используется в rust-analyzer, rustc) - система мемоизации зависимостей: пересчитывает только то что реально изменилось.
Почему инкрементальный парсинг критичен для language server?
Error-Tolerant Parsing
IDE должна предоставлять completions и hover даже в невалидном (незавершённом) коде. Error-tolerant парсер продолжает строить дерево после ошибки: вставляет синтетические токены, пропускает unexpected токены, строит partial AST. rustc_errors использует 'error recovery tokens'; TypeScript parser вставляет Missing nodes.
rustc_parse (Rust parser) имеет специальный 'recovery' режим: при встрече неожиданного токена parser пробует несколько стратегий восстановления. clangd восстанавливается после ошибок в шаблонах C++. Eclipse JDT (Java IDE) использует Error Recovery Parser с 1992 года - один из первых error-tolerant парсеров в IDE.
Как error-tolerant парсер обрабатывает незавершённый property access `obj.`?
IDE Features: реализация
Go-to-definition требует: парсинг + type checking + symbol resolution. Completions требуют: partial parse + type inference в контексте + ranking по usage. Find all references требует индексирования всего workspace. Rename - transactional изменение всех reference sites. Каждая фича строится поверх компиляторного frontend.
rust-analyzer обрабатывает workspace Rust проекта в ~2-5 секунд (initial analysis). Для крупных монорепо (таких как внутренние репозитории Google) language servers используют partial loading и on-demand analysis. clangd 16+ поддерживает 'remote index' для больших C++ кодовых баз: предварительно построенный индекс загружается из S3.
Language server - это просто компилятор запущенный в daemon режиме
Language server имеет принципиально другие требования: error tolerance, incremental updates, partial analysis, latency < 100ms. Обычный компилятор оптимизирован для batch compilation - несовместимые требования
rustc компилирует Rust за секунды или минуты - это нормально для batch build. rust-analyzer должен отвечать за 20ms при каждом нажатии клавиши. Это требует полностью другой архитектуры: Salsa incremental computation, partial type inference, error-tolerant parsing
Почему реализация 'rename' сложнее 'go-to-definition'?
Итоги
- LSP: JSON-RPC протокол между редактором и language server; 1 server * N редакторов вместо N*M плагинов
- Incremental parsing (Tree-sitter, Salsa): пересчёт только затронутых узлов при изменении; <1ms вместо 50ms для single edit
- Error-tolerant parsing: Missing/ERROR синтетические узлы позволяют completions и hover даже в невалидном незавершённом коде
Связанные темы
Language server реализует компиляторный frontend для интерактивного использования:
- Тестирование компиляторов — Language server тестируется через LSP test harness: отправка запросов и проверка ответов
- Написание компилятора — Language server = компиляторный frontend + LSP слой; те же парсер и type checker, другие требования по latency
- DSL компиляторы — DSL также могут иметь language server: SQL, Dhall, Nix имеют LSP реализации
Вопросы для размышления
- rust-analyzer - это отдельный проект от rustc компилятора. Почему не использовать rustc с --watch флагом? Какие принципиальные архитектурные отличия делают это невозможным?
- Tree-sitter парсер хранит CST (Concrete Syntax Tree) с пробелами и комментариями, а не AST. Почему это важно для IDE функциональности?
- Completions в Rust требуют type inference для определения методов объекта. Как rust-analyzer делает это быстро (<100ms) для файла с десятками использований generics?