RFC-017: Дизайн поддержки Language Server Protocol (LSP)
Справка: Смотрите полный пример о том, как писать RFC.
⚠️ Предварительные условия реализации (важно)
Перед реализацией LSP необходимо решить следующие две основные проблемы:
Проблема 1: Сбор диагностических ошибок
Текущее состояние: Типоизводитель в настоящее время возвращает результат при обнаружении первой ошибки (с помощью оператора ?), что не позволяет собирать все ошибки.
Требование LSP: IDE должна отображать все ошибки, а не только первую.
Решение:
1.1 Режим сбора ошибок
- Изменить модуль
src/frontend/typecheck/inference/для возвратаResult<Type, Vec<Error>> - Не возвращаться сразу при обнаружении ошибки, а продолжить проверку
- После завершения проверки вернуть все ошибки вместе
1.2 Уровни ошибок
Различать ошибки разной степени серьёзности:
enum ErrorKind {
Error, // Серьёзная ошибка, может вызвать каскадные ошибки
Warning, // Предупреждение, продолжить проверку без остановки
Note, // Дополнительная информация
}- Если есть
Error:publishDiagnosticsотображает ошибки - Если только
Warning: продолжить компиляцию, показать предупреждения
1.3 Восстановление ошибок парсера
- При ошибках парсинга вставлять placeholder-узлы (например,
MissingExpression) вместо отказа от разбора - Избегать паники типоизводителя из-за неполного AST
- Пример:
let x = ;→let x = MissingExpression
1.4 Отложенный отчёт (Delayed Emission)
- Некоторые ошибки могут быть «каскадными» (вызваны предыдущими ошибками)
- Можно сначала собрать их, а после разбора AST отфильтровать очевидные каскадные ошибки
- Или простое решение: сообщать обо всех, чтобы пользователь исправлял по порядку
Проблема 2: Кэширование разбора на уровне файлов
Текущее состояние: При каждом LSP-запросе весь файл разбирается заново, без механизма кэширования.
Требование LSP: Каждое редактирование должно обрабатываться быстро, без повторного разбора неизменённых файлов.
Решение:
2.1 Структура кэша документа
struct DocumentCache {
version: u32, // Версия документа LSP
content: String, // Текущее содержимое
content_hash: u64, // Хэш содержимого (быстрое сравнение)
ast: Option<Ast>, // Кэшированный AST (опционально)
}2.2 Обнаружение изменений
- При каждом получении
textDocument/didChangeнового содержимого - Вычислить хэш нового содержимого и сравнить с кэшированным
content_hash - Если изменилось: переразобрать весь файл
- Если не изменилось: вернуть кэшированный результат
2.3 Стратегия переразбора
- На уровне файла: переразбирать только текущий файл, а не весь проект
- Это упрощённый дизайн, без инкрементного разбора на уровне функций
- Современные компьютеры могут разобрать файл в несколько тысяч строк за миллисекунды
2.4 Отличие от cargo check
| cargo check | YaoXiang LSP | |
|---|---|---|
| Область | Весь проект | Один файл |
| Частота | Вручную | При каждом редактировании |
| Цель | Полная проверка компиляции | Быстрый инкрементный ответ |
Интеграция с существующими модулями
| Существующий модуль | Способ интеграции LSP |
|---|---|
util/span.rs | ✅ Уже есть Position/Span, напрямую маппится в LSP Position |
util/diagnostic/collect.rs | ⚠️ Изменить в «режим сбора», непрерывно накапливать ошибки |
frontend/core/lexer/symbols.rs | ⚠️ Расширить, добавить информацию о позиции uri + span |
frontend/typecheck/mod.rs | ⚠️ Изменить TypeResult, возвращать все ошибки |
frontend/core/parser/ast.rs | ✅ У каждого узла уже есть Span, изменения не нужны |
Резюме
Добавить поддержку Language Server Protocol (LSP) в YaoXiang, реализовать полноценный языковой сервер, чтобы основные IDE (VS Code, Neovim, Emacs и другие) могли предоставлять функции разработки: дополнение кода, переход к определению, диагностику, поиск ссылок и другие.
Мотивация
Зачем нужна эта функция?
В настоящее время язык YaoXiang не имеет официальной поддержки интеграции с IDE, разработчики могут использовать только базовые текстовые редакторы для написания кода, что лишает их:
- Дополнение кода — невозможность интеллектуально дополнять идентификаторы, ключевые слова, типы на основе контекста
- Переход к определению — невозможность быстро перейти к месту определения функции, типа, переменной
- Оперативная диагностика — невозможность мгновенно отображать синтаксические и типовые ошибки при редактировании
- Поиск ссылок — невозможность найти все места использования символа
- Всплывающие подсказки — невозможность отображать информацию о типах и документацию при наведении курсора
LSP — это стандарт современных языков программирования, основные языки (Rust, Python, TypeScript, Go и другие) имеют成熟的 реализации LSP. Поддержка LSP значительно улучшит опыт разработки на YaoXiang.
Текущие проблемы
- Низкая эффективность разработки — отсутствие дополнения кода и интеллектуальных подсказок
- Сложность отладки — невозможность быстро найти определение символа
- Крутая кривая обучения — отсутствие вспомогательных функций IDE
- Несовершенная экосистема — невозможность привлечь разработчиков, привыкших к современным IDE
Предложение
Основной дизайн
Реализовать отдельный процесс LSP-сервера, который общается с IDE через JSON-RPC:
flowchart TD
subgraph IDE_Environment [Среда IDE]
IDE["IDE (VS Code)"]
end
subgraph LSP_Server [LSP сервер]
LSP["YaoXiang LSP Server"]
end
subgraph World_Compile [Компиляционный мир World]
direction TB
W_Symbol["Symbol Index"]
W_Type["Type Env"]
W_Diag["Diagnostics"]
end
subgraph Cache [Кэш документа Document Cache]
direction TB
C_Version["Управление версиями"]
C_Content["Кэш содержимого"]
C_AST["Кэш AST"]
C_Delta["Область инкрементных изменений"]
end
subgraph Frontend [Интерфейсный компилятор Compiler Frontend]
direction TB
F_Lexer["Lexer (util/span.rs Position)"]
F_Parser["Parser (ast.rs уже имеет Span)"]
F_TypeCheck["Type Check (изменить в режим сбора)"]
F_ErrorCollector["ErrorCollector (util/diagnostic/)"]
end
IDE <-->|JSON-RPC| LSP
LSP --- World_Compile
LSP --- Cache
Cache -- "Инкрементное обновление" --> World_Compile
World_Compile --- Frontend
Cache --- FrontendАрхитектура LSP-сервера
src/lsp/
├── main.rs # Точка входа LSP сервера
├── server.rs # Основная логика сервера
├── session.rs # Управление сессиями
├── capabilities.rs # Объявление возможностей сервера
├── handlers/
│ ├── mod.rs
│ ├── initialize.rs # Обработка инициализации
│ ├── text_document.rs # Обработка операций с документами
│ ├── completion.rs # Обработка дополнения
│ ├── definition.rs # Обработка перехода к определению
│ ├── references.rs # Обработка поиска ссылок
│ ├── hover.rs # Обработка всплывающих подсказок
│ └── diagnostics.rs # Обработка диагностики
├── world.rs # Компиляционный мир (таблица символов, кэш AST)
├── scroller.rs # Построение индекса символов
├── protocol.rs # Определения типов протокола LSP
└── cache/ # Модуль инкрементного кэша (новый)
├── mod.rs
├── document.rs # Кэш документа (версия, AST, таблица символов)
└── incremental.rs # Стратегия инкрементного разбораДизайн компиляционного мира (World)
Управление глобальным состоянием компиляции:
- Кэш документа (версия, AST, таблица символов)
- Глобальный индекс символов
- Сборщик ошибок
- Кэш окружения типов
Основные методы:
on_document_change: обработка инкрементных измененийincremental_reparse: инкрементный переразборcollect_diagnostics: сбор всех ошибок (без остановки)
Поддержка основных методов LSP
| Категория | Метод | Описание |
|---|---|---|
| Жизненный цикл | initialize / initialized / shutdown / exit | Жизненный цикл сервера |
| Синхронизация документов | didOpen / didChange / didClose | Управление документами |
| Диагностика | publishDiagnostics | Публикация диагностики |
| Дополнение | completion | Дополнение кода |
| Переход | definition | Переход к определению |
| Ссылки | references | Поиск ссылок |
| Hover | hover | Всплывающие подсказки |
| Символы | workspace/symbol | Поиск символов в рабочей области |
Механизм синхронизации текстовых документов
Используется стратегия инкрементной синхронизации:
- Сохранение номера версии документа
- Применение инкрементных изменений (range + text)
- При больших изменениях — переход на полную замену
Построение индекса символов
Используя существующую систему таблицы символов, построить обратный индекс:
- Нужно расширить
SymbolEntry, добавить полеlocation - Индекс: имя → список позиций, файл → список символов
Реализация дополнения кода
Источники дополнения: ключевые слова, переменные, функции, типы, поля структур, модули
Реализация перехода к определению
Символьное разрешение на основе AST: поиск позиции определения, соответствующей идентификатору/вызову функции
Детальный дизайн
Влияние на систему типов
- Расширение символьной информации — добавление позиционной информации (файл, строка, столбец) в таблицу символов
- Раскрытие информации о типах — предоставление интерфейса запросов типов для LSP
- Интеграция документации — поддержка генерации документации из комментариев
Поведение во время выполнения
- LSP-сервер работает как отдельный процесс
- Использует stdin/stdout для JSON-RPC коммуникации
- Поддержка параллельной обработки нескольких сессий
Изменения в компиляторе
| Компонент | Изменения |
|---|---|
frontend/events | Расширить систему событий, добавить уведомления LSP |
frontend/core/lexer/symbols | Усилить таблицу символов, добавить позиционную информацию |
Новый src/lsp/ | Реализация LSP сервера |
Обратная совместимость
- ✅ Полная обратная совместимость
- LSP-сервер — независимый компонент, не влияет на существующий процесс компиляции
- Существующие инструменты CLI не затрагиваются
Интеграция с существующими системами
- Система событий — использовать механизм подписки на события
frontend/events/ - Диагностическая система — переиспользовать диагностический вывод
util/diagnostic/- Переиспользовать
ErrorCollector<E>для сбора всех ошибок - Преобразовать
Diagnosticв формат LSPDiagnostic
- Переиспользовать
- Таблица символов — расширить возможности позиционирования символов в
symbols.rs- Расширить
SymbolEntry, добавить полеlocation: Location - Построить обратный индекс
SymbolIndex(имя -> список позиций)
- Расширить
- Интерфейсный компилятор — напрямую вызывать Lexer, Parser, типоизводитель
- Ключевое изменение: типоизводитель должен работать в «режиме сбора», без остановки
Преобразование формата диагностики
/// Преобразование YaoXiang Diagnostic в LSP Diagnostic
fn to_lsp_diagnostic(diag: &Diagnostic) -> lsp_types::Diagnostic {
let severity = match diag.severity() {
Severity::Error => lsp_types::DiagnosticSeverity::ERROR,
Severity::Warning => lsp_types::DiagnosticSeverity::WARNING,
Severity::Info => lsp_types::DiagnosticSeverity::INFORMATION,
};
lsp_types::Diagnostic {
range: to_lsp_range(diag.span()),
severity: Some(severity),
message: diag.message().to_string(),
code: diag.code().map(|c| lsp_types::NumberOrString::String(c.as_string())),
..Default::default()
}
}
/// Преобразование YaoXiang Span в LSP Range
fn to_lsp_range(span: &Span) -> lsp_types::Range {
lsp_types::Range {
start: lsp_types::Position {
line: span.start.line.saturating_sub(1), // LSP использует 0-indexed
character: span.start.column.saturating_sub(1),
},
end: lsp_types::Position {
line: span.end.line.saturating_sub(1),
character: span.end.column.saturating_sub(1),
},
}
}Уникальные расширенные функции YaoXiang
Используя мощную систему вычислений во время компиляции и владения YaoXiang, предоставить уникальный опыт разработки, недоступный в других языках:
1. Встроенные подсказки (Inlay Hints)
- Подсказки значений констант: отображение вычисленных во время компиляции значений (например, рядом с
const MAX = 100 + 200показывать300) - Подсказки изменяемости: отображение, является ли переменная изменяемой (например,
mut x,xс очевидным подчёркиванием) - Подсказки потребления владения: отображение, потребляется ли параметр функции (например,
consumed/borrowed) - Подсказки семантики пустого владения: затемнение цвета переменной для подсказки о возможности переприсваивания после перемещения
- Подсказки выведенного типа: отображение конкретного выведенного типа (например, рядом с
x = vec![]показыватьVec<i32>)
2. Визуализация семантики владения
- Отображение пути перемещения переменной (от позиции определения до всех позиций использования)
- Визуализация времени жизни заимствования
3. Предпросмотр вычислений во время компиляции
- Hover отображает результат вычисления константных выражений во время компиляции
Приоритеты реализации
| Функция | Приоритет |
|---|---|
| Подсказки значений констант | P0 |
| Подсказки изменяемости | P0 |
| Подсказки потребления владения | P1 |
| Визуализация владения | P2 |
Коммуникация и удалённая поддержка
Режимы коммуникации
Поддержка трёх режимов:
| Режим | Использование |
|---|---|
| stdio | Локальная разработка (по умолчанию) |
| TCP Socket | Удалённая разработка/отладка |
| Unix Domain Socket | Высокопроизводительная локальная коммуникация |
Удалённая отладка
На основе DAP (Debug Adapter Protocol):
- Поддержка точек останова на строках, функциях, условных точек останова
- Уникальные точки останова YaoXiang: срабатывание при перемещении переменной
Параметры запуска
# Локальный режим
yaoxiang-lsp
# TCP сервер
yaoxiang-lsp --tcp --port 8765
# С включением отладки
yaoxiang-lsp --tcp --port 8765 --enable-debugМодель параллелизма
Дизайнерское решение: однопоточный + асинхронный цикл событий
Обоснование:
- Компилятор не является потокобезопасным, стоимость переделки высока
- LSP-запросы по своей природе последовательны, не требуют параллелизма
- Однопоточность проще и легче для отладки
- Производительности async I/O в одном потоке достаточно
Фоновые задачи используют spawn_blocking для задействования нескольких ядер.
Встроенный инструмент тестирования LSP (опционально)
Эта функция не является обязательной для MVP, может быть добавлена в последующих версиях.
Формат JSON тестовых случаев:
# Запуск тестов
yaoxiang-lsp --testКомпромиссы
Преимущества
- Улучшение опыта разработки — поддержка IDE, близкая к основным языкам
- Совершенствование экосистемы — привлечение большего числа разработчиков для YaoXiang
- Повышение качества кода — оперативная диагностика уменьшает ошибки времени выполнения
- Вклад сообщества — разработчики могут участвовать в разработке инструментария LSP
Недостатки
- Высокая сложность реализации — необходимо обрабатывать большое количество граничных случаев LSP
- Стоимость поддержки — необходимо следовать обновлениям версий протокола LSP
- Вопросы производительности — производительность индексации и запросов для крупных проектов
- Сложность тестирования — требуется моделирование поведения IDE для тестирования
Альтернативные решения
| Решение | Почему не выбрано |
|---|---|
| Только подсветка синтаксиса | Не соответствует современным потребностям разработки |
| Использование Tree-sitter | Требует дополнительных затрат на изучение, возможности ограничены |
Стратегия реализации
Фазы разделения
Фаза 0 (предварительная): Адаптация компилятора ⚠️ Критически важно
- Изменить типоизводитель в «режим сбора», вернуть
Result<Type, Vec<Error>> - Реализовать уровни ошибок (Error / Warning / Note)
- Восстановление ошибок парсера: вставлять placeholder-узлы
- Расширить таблицу символов
SymbolEntry, добавить полеlocation - Реализовать систему кэширования DocumentCache (версия + содержимое + хэш)
- Эта фаза является предпосылкой реализации LSP, должна быть выполнена в первую очередь
- Изменить типоизводитель в «режим сбора», вернуть
Фаза 1 (v0.7): Базовая структура
- Скелет LSP-сервера
- Методы жизненного цикла (initialize/shutdown/exit)
- Базовая логирование и обработка ошибок
Фаза 2 (v0.7): Поддержка диагностики
- Синхронизация текстовых документов
- Интеграция диагностики компилятора
textDocument/publishDiagnostics
Фаза 3 (v0.8): Поддержка дополнения
- Построение индекса символов
- Дополнение ключевых слов
- Дополнение идентификаторов
Фаза 4 (v0.8): Поддержка перехода
- Переход к определению
- Поиск ссылок
- Всплывающие подсказки
Фаза 5 (v0.9): Расширенные функции
- Поиск символов в рабочей области
- Форматирование кода
- Поддержка рефакторинга (опционально)
Зависимости
- Нет внешних зависимостей от библиотек LSP (используется crate
lsp-types) - Зависимость от существующих модулей интерфейсного компилятора
- Зависимость от
serde_jsonдля сериализации JSON-RPC
Риски
- Проблемы производительности — разбор больших файлов может вызвать зависание
- Решение: инкрементный разбор, обработка в фоновом потоке
- Использование памяти — индекс символов занимает память
- Решение: ленивая загрузка, LRU-кэш
- Совместимость протокола — различия версий LSP
- Решение: объявить поддерживаемую версию протокола
Открытые вопросы
- [x] Механизм сбора ошибок (см. раздел «Предварительные условия реализации»)
- [x] Система инкрементного кэширования (см. раздел «Предварительные условия реализации»)
- [x] Версия протокола LSP: использовать 3.18 (поддержка Inlay Hints, Inline Values и других новых функций)
- [x] Удалённая коммуникация (через TCP, с учётом LSP + отладка)
- [x] Удалённая отладка (на основе протокола DAP)
- [x] Модель параллелизма: однопоточный + async цикл событий
- [x] Встроенный инструмент тестирования LSP (опционально): использование JSON тестовых случаев
Приложения (опционально)
Приложение A: Записи обсуждения дизайна
Используется для записи детального обсуждения в процессе принятия дизайнерских решений.
Приложение B: Записи дизайнерских решений
| Решение | Решение | Дата | Автор |
|---|---|---|---|
| Архитектура LSP сервера | Независимый процесс, коммуникация через stdio | 2026-02-15 | 晨煦 |
| Версия протокола | Поддержка LSP 3.18 (нужны Inlay Hints и другие новые функции) | 2026-02-22 | 晨煦 |
| Режим сбора ошибок | Возврат Result<Type, Vec<Error>>, поддержка уровней ошибок и восстановления | 2026-02-22 | 晨煦 |
| Стратегия кэширования | Кэширование на уровне файлов: версия + содержимое + хэш, переразбор всего файла | 2026-02-22 | 晨煦 |
| Режим коммуникации | Поддержка stdio + TCP + UnixSocket | 2026-02-22 | 晨煦 |
| Удалённая отладка | На основе протокола DAP, общий транспортный слой с LSP | 2026-02-22 | 晨煦 |
| Модель параллелизма | Однопоточный + async цикл событий | 2026-02-22 | 晨煦 |
| Инструмент тестирования (опционально) | JSON тестовые случаи + встроенный тестовый раннер | 2026-02-22 | 晨煦 |
Приложение C: Глоссарий
| Термин | Определение |
|---|---|
| LSP | Language Server Protocol, протокол языкового сервера |
| JSON-RPC | JSON-Remote Procedure Call, удалённый вызов процедур JSON |
| DAP | Debug Adapter Protocol, протокол адаптера отладки |
| Индекс символов | Таблица отображения позиций символов, построенная во время компиляции |
| Компиляционный мир | Контекст, содержащий всю информацию о компиляции |
| Встроенные подсказки | Inlay Hints, информационные подсказки, отображаемые в строке |
| Отслеживание владения | Ownership Trace, визуализация потока владения переменной |
Список литературы
- Спецификация Language Server Protocol
- Спецификация LSP 3.18
- Спецификация Debug Adapter Protocol
- Rust Analyzer — эталонная реализация
- crate lsp-types — определения типов LSP
- Спецификация JSON-RPC 2.0
Жизненный цикл и судьба
RFC имеет следующие состояния:
┌─────────────┐
│ Черновик │ ← Создаётся автором
└──────┬──────┘
│
▼
┌─────────────┐
│ Наreview │ ← Обсуждение сообщества
└──────┬──────┘
│
├──────────────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Принят │ │ Отклонён │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ accepted/ │ │ rejected/ │
│ (утверждён) │ │ (отклонён) │
└─────────────┘ └─────────────┘Описание состояний
| Состояние | Расположение | Описание |
|---|---|---|
| Черновик | docs/design/rfc/draft/ | Черновик автора, ожидает отправки на review |
| Наreview | docs/design/rfc/review/ | Открыто обсуждение и обратная связь сообщества |
| Принят | docs/design/accepted/ | Становится официальным документом дизайна, переходит в фазу реализации |
| Отклонён | docs/design/rfc/ | Остаётся в каталоге RFC, обновляется состояние |
Действия после принятия
- Переместить RFC в каталог
docs/design/accepted/ - Обновить имя файла на описательное (например,
lsp-support.md) - Обновить статус на «Официальный»
- Обновить статус на «Принято», добавить дату принятия
Действия после отклонения
- Оставить в каталоге
docs/design/rfc/draft/ - Добавить причину отклонения и дату в верхнюю часть файла
- Обновить статус на «Отклонён»
Действия после определения обсуждения
Когда по某个 открытому вопросу достигнут консенсус:
- Обновить приложение A: заполнить «Решение» под темой обсуждения
- Обновить основной текст: синхронизировать решение в тексте документа
- Записать решение: добавить в «Приложение B: Записи дизайнерских решений»
- Отметить вопрос: поставить галочку
[x]в списке «Открытые вопросы»
Примечание: Номер RFC используется только на этапе обсуждения. После принятия номер удаляется, используется описательное имя файла.
