Компиляторы

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?

Связанные уроки

  • sd-10-microservices
Language Server Protocol

0

1

Войти