Представлен проект, над которым велась работа последние 4 месяца: альтернативная реализация Rust LSP, построенная с фокусом на низкое потребление памяти.

У неё две главные особенности:

  • Очень низкое потребление памяти (цель — менее 100 МБ для типовых проектов). Есть оговорки, они описаны ниже.
  • Мгновенная индексация после перезапуска: если проект уже был проиндексирован, перезапуск редактора не потребует повторной индексации.

Примечание: на протяжении всей демонстрации потребление RAM оставалось ниже 100 МБ

Потребление памяти Rust Glancer остаётся ниже 100 МБ

Эти особенности делают Rust Glancer подходящим для не самых новых компьютеров: инструмент тестировался на старом MacBook Pro M1 2020 с 8 ГБ RAM, и результаты оказались вполне достойными.

МашинаLSPБазовая индексация (движок пригоден к работе)Полная индексация
MacBook Pro M4 Max, 36GB (2025)Rust Glancer5 секунд8 секунд
MacBook Pro M4 Max, 36GB (2025)rust-analyzer6 секунд13 секунд
MacBook Pro M1, 8GB (2020)Rust Glancer6 секунд9 секунд
MacBook Pro M1, 8GB (2020)rust-analyzer7 секунд14 секунд

4 месяца — не так много времени для проекта такого масштаба, как Rust LSP. Rust Glancer пока не является полноценной реализацией: не хватает функциональности, есть известные баги и множество вещей, которые предстоит улучшить.

В то же время инструмент уже достаточно функционален: реализован полный конвейер индексации с выводом типов и решателем трейтов (chalk), поддерживается большая часть «обычного» синтаксиса Rust, работают и большинство стандартных действий LSP — переход к определению, hover, inlay hints, автодополнение и прочее.

Попробовать можно уже сейчас: достаточно установить расширение для VS Code здесь, либо, при желании, собрать и установить vsix из репозитория.

Далее — предыстория проекта: мотивация, использование LLM, планы и дорожная карта. Тем, кто хочет сразу перейти к техническим деталям, стоит заглянуть в документацию проекта.

Отличия от rust-analyzer

Есть несколько причин, почему rust-analyzer потребляет много памяти:

  1. Rust-проекты содержат действительно большой объём информации, который нужно индексировать: тысячи функций, структур, трейтов, связи между ними, тела функций и содержащиеся в них выражения и так далее. Всё это нужно анализировать и запоминать, и здесь трудно «схитрить», если хочется поддерживать что-то вроде «найти все использования этой структуры».
  2. rust-analyzer использует в качестве базы данных salsa — инкрементальную query-based систему, которая лениво вычисляет все нужные данные без явной «записи» всего подряд. Подход очень изящный, но он неразрывно связан с оперативной памятью, что затрудняет вынос части данных куда-либо ещё.
  3. Для представления синтаксического дерева rust-analyzer использует rowan. Полезное свойство этой библиотеки — частичная инвалидация: если изменилась только часть файла, перепарсить нужно лишь соответствующие фрагменты, что быстрее, чем перепарсивать весь файл при каждом нажатии клавиши. Однако древовидное представление внутри может вызывать сильную фрагментацию памяти (то есть объём RAM, забираемый у ОС, оказывается выше, чем объём «реально используемой» памяти).

С пунктом (1) приходится мириться (хотя кое-какие оптимизации возможны, и Rust Glancer их применяет), а вот (2) и (3) — следствия архитектуры rust-analyzer. Эти решения были приняты, чтобы сделать LSP быстрее, и со своей задачей они справляются.

Идея, с которой начинался проект: что если не пытаться делать инкрементальный LSP? Что если вместо этого иметь замороженный результат анализа, который инвалидируется при сохранении? Это очевидно будет не так быстро, как в rust-analyzer, но даст нужные свойства:

  1. Результаты анализа можно вынести в файловую систему и загружать в память только тогда, когда они действительно нужны.
  2. Сохранённый анализ можно переиспользовать, и поскольку он уже вынесен в файловую систему, его можно использовать повторно после перезапуска редактора.

Это и есть базовая идея Rust Glancer.

Проект индексируется один раз, результаты сохраняются в файловой системе, и когда запросам что-то нужно, они подгружают необходимую информацию на время выполнения запроса.

Бесплатно это не даётся: замороженный анализ проекта по определению медленнее ленивого инкрементального, поскольку загрузка и десериализация данных из файловой системы медленнее, чем загрузка из памяти. Чтобы это компенсировать, Rust Glancer прибегает к некоторым трюкам: например, при вводе текста не выполняется полный анализ на каждое нажатие клавиши — вместо этого делается поверхностный анализ текущего тела функции с переиспользованием предыдущего полного индекса. Это делает автодополнение достаточно быстрым, но означает, что новые элементы (импорты, структуры, трейты) не «индексируются», пока документ не будет сохранён. Это не должно стать проблемой: привыкание проходит быстро, и, по личному опыту, спустя какое-то время это не ощущается как что-то неправильное. Тем, кому это кажется пугающим, стоит просто попробовать — на деле всё не так страшно.

Для тех, кто полагается на агентные рабочие процессы, Rust Glancer также оптимизирован под большой объём изменений, вносимых вне редактора. По неясным причинам в rust-analyzer наблюдалась ситуация, когда при правках кода агентами inlay hints съезжали не на свои места; та же проблема поначалу возникала и в Rust Glancer, но была решена с помощью собственного файлового вотчера и его тонкой настройки. Кроме того, сервер присваивает изменениям вне редактора более низкий приоритет, так что агентные правки не вызывают резкую переиндексацию.

Тем не менее важно понимать: у Rust Glancer есть свои преимущества, но по сравнению с rust-analyzer есть и недостатки (помимо очевидной незавершённости). Часть из них, возможно, удастся решить со временем, но крайне маловероятно, что Rust Glancer когда-либо станет «таким же, как rust-analyzer, но лучше». Похоже, rust-analyzer останется выбором по умолчанию для проектов, которым важны полнота и точность реакции на каждое нажатие клавиши, а Rust Glancer подойдёт тем, у кого более слабые машины, или тем, кто готов на определённые жертвы ради снижения потребления памяти.

Как и почему это случилось

Профессиональная работа с Rust ведётся около 7 лет, и довольно рано пришёл интерес к тому, как разрабатываются компилятор и сопутствующие инструменты. Были сделаны небольшие вклады в rustc, clippy и rust-analyzer, а также потрачены десятки часов на чтение исходного кода просто ради самообучения. Так что масштаб проекта под названием «Rust LSP» был вполне ясен заранее.

При этом отношения с rust-analyzer можно назвать любовью-ненавистью. Он прекрасен во всём, кроме двух вещей: потребления памяти и первоначальной индексации (особенно при включённых build-скриптах и proc-макросах). Эти проблемы упоминаются довольно часто, но в данном случае они особенно ощутимы из-за не самого разумного рабочего процесса: одновременно открыты две одинаковые IDE на двух мониторах с кучей проектов внутри workspace. Из-за этого потребление памяти примерно удваивается, и с последним набором проектов rust-analyzer потреблял 16 ГБ памяти, которые куда приятнее было бы использовать для других задач; не говоря уже о том, что при каждом запуске VS Code вентиляторы компьютера начинали шуметь из-за кучи параллельных задач индексации.

В какой-то момент возникла мысль: раз знание Rust достаточно уверенное, полноценный LSP, вероятно, и не нужен — можно обойтись чем-то более простым и экономным по памяти. Была предпринята попытка собрать «умный ctags для Rust». Осознанно не ставилась цель построить альтернативный LSP — настолько безумной казалась эта задача. Как оказалось, зря...

Начало продвигалось довольно гладко: использовалась библиотека синтаксиса rust-analyzer, элементы понижались до внутренних представлений, строились карты определений и структура модулей, все объявления индексировались. Всё было настолько просто, что появилось желание сделать примитивное понижение тел функций. Затем захотелось добавить совсем простое распространение типов. Оказалось, что наивное распространение типов даёт не так много — но inlay hints уже были готовы и получались приятными, так что хотелось большего. В целом ведь сложные случаи и nightly-фичи не особо важны, верно? (Верно?..) Так появилось наивное разрешение трейтов через сопоставление заголовков impl. Затягивает, что поделать.

Иллюзия простоты, однако, разрушилась, когда стало ясно, что следующий код тоже разумно ожидать поддерживаемым:

1fn mul_by_two(vals: &[u8]) -> Vec<u8> {
2    vals.iter().copied().map(|v| v * 2).collect()
3}

Код довольно простой, но чтобы его поддержать, нужны:

  • Поддержка типа-среза (slice)
  • Замыкания / трейты Fn
  • Решение трейтов
  • Проекция ассоциированных типов
  • Куча nightly-фич

последний пункт особенно забавен: желание избежать nightly было, но не учитывалось, что сам std (да и sysroot в целом) буквально «дышит» nightly. Вот так вот.

В итоге, шаг за шагом, «умный ctags» постепенно превращался в «настоящий LSP». Пожалуй, три самых значимых вехи:

  1. Раскрытие декларативных макросов (после этого опыта декларативные макросы вызывают лишь раздражение). К счастью, удалось переиспользовать большую часть инфраструктуры rust-analyzer для этой задачи.
  2. Полноценный движок вывода типов. Момент настоящего «ага!» настал, когда пришло понимание, как на самом деле работает вывод типов (вкратце: все связанные привязки типов «сцепляются» в большой таблице вывода, а затем система пытается получить свидетельства из всех возможных мест, где такое свидетельство способно решить типы сразу для нескольких мест). Пожалуй, именно этот момент принёс больше всего удовольствия за всё время работы над проектом.
  3. Полноценный движок решения трейтов. Изначально предполагалось, что «решатель трейтов в этом проекте вряд ли понадобится», но затем очень захотелось заставить упомянутый выше пример с итератором работать как следует. Какое-то время интеграции решателя трейтов удавалось избегать, полагаясь на наивные хаки вроде наивного сопоставления impl-трейтов и специализированных обработчиков для трейтов std, но со временем это становилось всё сложнее и работало всё хуже. В итоге было решено интегрировать Chalk, который оказался заметно проще всей построенной ранее иерархии. Хотя заставить Chalk работать быстро — это была отдельная задача.

Отдельно стоит отметить то, чем гордиться приходится больше всего (и что, собственно, сделало Rust Glancer возможным — не будь этого спроектировано на раннем этапе, проект бы очень быстро умер) — это удобный стек для профилирования, позволяющий измерять производительность, потребление памяти (как нативно, отслеживая реально выделенные объекты, так и через jemalloc), профилировать данные по запросу и сравнивать LSP с rust-analyzer, а также набор бенчмарков, запускаемых в CI. Частично это описано в документации (1, 2), более подробное описание появится позже.

Примерно 1,5 месяца назад Rust Glancer стал основным повседневным инструментом вместо rust-analyzer. Текущее состояние проекта уже достаточно хорошо, чтобы представить его широкой аудитории.

Использование LLM

Проект создавался с активным использованием LLM. При этом речь не идёт о «vibe coding» — каждый pull request проверяется, чтобы убедиться в приемлемом состоянии кодовой базы. Убедиться в этом можно, посмотрев git-историю: там есть PR с diff'ами в 10 тысяч с лишним строк, но между ними проходит по несколько дней, несмотря на то что работа над проектом ведётся практически ежедневно с самого начала. Код небезразличен, и было бы странно потратить 4 месяца на создание Rust LSP, если бы чтение и вникание в код не были частью повседневной практики.

Нет претензии на статус опытного разработчика LSP и на идеальность кода. Состояние кода вполне рабочее, но некоторые участки, вероятно, не идиоматичны с точки зрения дизайна инструментов для компиляторов. В коде много комментариев, и приложены серьёзные усилия, чтобы они были не формальными, а действительно полезными — ведь читать их приходится постоянно; качество пока явно уступает профессионально написанной человеком документации, но, на субъективный взгляд, комментарии вполне полезны и не раздражают при чтении.

Значительная часть пути — это обучение. LLM неплохо разбираются в предметной области и знают о дизайне LSP куда больше. При этом с построением больших проектов у LLM дела обстоят не так хорошо. Поэтому в ходе разработки многократно повторялся один и тот же цикл:

  1. Строится что-то новое.
  2. Предложения LLM выглядят разумно, и они принимаются.
  3. Всё работает, но что-то смущает.
  4. После некоторых раздумий над дизайном обнаруживается серьёзный изъян.
  5. Совместно с LLM изъян исправляется (иногда на это уходит неделя, если промах был особенно крупным — но чем крупнее промах, тем больше удаётся из него вынести).

С одной стороны, если приписывать авторство кода именно LLM, можно было бы жаловаться: «LLM столько раз пытались пустить проект под откос!». Но поскольку код — свой, кажется более правильным признать: иногда качество кода временно проседает, но по мере обучения удаётся его улучшать. Это вполне нормальный процесс разработки ПО, просто ускоренный.

В конечном счёте LLM — это всего лишь инструмент, и каждый сам выбирает, использовать его ответственно или перекладывать на него собственное мышление. Учитывая нынешний уровень «охоты на ведьм» вокруг темы, есть лишь одна просьба: не сводить всё к ярлыку «клэнкера». Это свой код, и если кто-то считает его халтурой — пусть называет её своей халтурой, а не «ИИ-халтурой».

Критика приветствуется, и обратная связь будет с интересом изучена: чем больше удаётся узнать, тем лучше получается улучшать кодовую базу. А используются ли для этого LLM или нет — не так уж и важно.

Что дальше

Проект уже находится в состоянии, пригодном для повседневного использования частью пользователей, но планы на будущее довольно масштабные. В ближайших релизах стоит ожидать:

  • Дальнейшие оптимизации производительности
  • Дополнительные оптимизации памяти (в первую очередь на этапе индексации, плюс есть несколько проблем с фрагментацией после полного прогона индексации, которые предстоит исправить)
  • Улучшенный вывод типов и расширенную поддержку синтаксиса.
  • Code actions (реализация недостающих полей трейтов, авто-импорты и так далее).
  • Потенциально — поддержку proc-макросов (есть не совсем обычная идея, не требующая реального выполнения кода, но на её проработку уйдёт время).

Некоторые возможности вряд ли появятся вовсе, например поддержка build-скриптов и proc-макросов через реальный вызов proc-макроса (то есть всё, что требует выполнения непроверенного кода). Также не планируется заниматься тем, что не критично на текущем этапе проекта, например переходом на новый решатель трейтов. Нишевые вещи вроде отдельных nightly-фич, вероятно, будут отложены до тех пор, пока проект не достигнет определённой степени зрелости на стабильном Rust.

Кроме того, в Rust Glancer реализовано немало интересных мелких трюков, которыми хочется гордиться (выравнивание времени жизни аллокаций для снижения фрагментации памяти, модель движка как отдельного подпроцесса — она помогает и с фрагментацией памяти, и с проектами из нескольких workspace, шардированный кэш и другое); если тема заинтересует читателей, будут написаны отдельные материалы о внутреннем устройстве Rust Glancer. Частично это уже описано в документации (1, 2), если хочется узнать подробности прямо сейчас.

В любом случае есть надежда, что проект уже сейчас полезен некоторым разработчикам, а в будущем окажется полезен ещё большему числу людей.