Rust стабилизировал never-тип после двух лет работы

В компиляторе Rust давно существовал внутренний never-тип, обозначаемый восклицательным знаком (!): он помечает функции, которые никогда не завершаются обычным образом (например, бесконечный цикл), и места, где значение в принципе не может появиться. Долгое время это была нестабильная возможность, доступная только внутри самого компилятора. 24 августа участник разработки Rust с ником waffle довёл работу по её стабилизации до конца, процесс занял больше двух лет, потому что затрагивал небольшое обратно несовместимое изменение в предыдущих редакциях (editions) языка, и авторам компилятора нужно было убедиться, что это не сломает много реального кода.

У never-типа две причины существования. Практическая: он делает обобщённый (generic) код эффективнее. Например, трейт FromStr из стандартной библиотеки возвращает Result с типом ошибки Err; если преобразование заведомо не может провалиться (как для ByteString), можно указать type Err = !, тогда компилятор знает, что ветка с ошибкой недостижима, и может полностью выкинуть код, который её проверяет. Философская причина связана с выводом типов: конструкции вроде if и while в Rust, это выражения со значением, и компилятору нужен какой-то тип для результата бесконечного цикла; never-тип закрывает этот случай единообразно, без специальных правил, а заодно автоматически приводится к любому другому типу, это безопасно, поскольку до кода с never-значением в принципе невозможно дойти.

Основная сложность была в правиле «отката» (fallback) для never-типа: когда компилятор не может однозначно вывести конкретный тип выражения, он подставляет тип по умолчанию. До редакции 2024 года этим типом по умолчанию был () (unit-тип), а в редакции 2024 года его заменили на сам never-тип, то есть фактически отменили неявное преобразование. Отдельная проблема, стандартный тип Infallible, который годами использовался как обходной путь, пока never-тип оставался нестабильным: он служил той же цели, но без поддержки со стороны компилятора, из-за чего оптимизатор не всегда мог убрать связанный с ним код. Планировалось, что после стабилизации Infallible станет псевдонимом never-типа, но выяснилось, что и это изменение технически обратно несовместимо, впрочем, оно почти полностью компенсирует изменение отката по типу, так что вместе оба изменения ломают заметно меньше кода, чем по отдельности.

Чтобы оценить реальный масштаб поломки, waffle в апреле прогнал инструмент crater, который скачивает и компилирует все публичные библиотеки с crates.io. Из 3300 затронутых крейтов полностью сломанными оказались только семь; остальные ломались лишь потому, что зависели от устаревших версий других библиотек, которые уже были исправлены. С 2024 года компилятор предупреждал о коде, который пострадает от будущего изменения отката, так что у авторов библиотек было время подготовиться заранее. Waffle также напрямую работал с авторами популярных библиотек над бэкпортом простых исправлений в старые версии; часть авторов согласилась, часть отказалась, посчитав старые версии библиотек снятыми с поддержки, но в сумме бэкпорты закрыли 1553 из проблемных крейтов.

В итоге, начиная с Rust 1.99, never-тип стабилен, а Infallible стал его псевдонимом. Тем, у кого код всё же перестанет собираться, доступны три варианта: остаться на версии Rust 1.98, обновить зависимости до версий с исправлением, либо явно указать тип возврата в проблемном месте вызова (например, foo::<()>()?). Авторы материала описывают ситуацию двояко: с одной стороны, это обратно несовместимое изменение, ломающее ранее стабильный код, а с другой, пример того, как Rust заранее предупреждает о будущих поломках, помогает экосистеме подготовиться и лично помогает с бэкпортами, что можно считать подтверждением приверженности обратной совместимости, а не её нарушением.

Ключевые факты

  • 24 августа участник разработки Rust с ником waffle завершил стабилизацию never-типа (!), работа заняла больше двух лет из-за небольшого обратно несовместимого изменения в старых редакциях языка
  • Начиная с Rust 1.99 never-тип стабилен, а стандартный тип Infallible становится его псевдонимом
  • Апрельский прогон инструмента crater по всем библиотекам crates.io нашёл 3300 затронутых крейтов, из которых полностью сломанными оказались только семь
  • Бэкпорты исправлений в старые версии библиотек, часть из которых сделал сам waffle совместно с их авторами, закрыли 1553 из проблемных крейтов
  • Пострадавшим от изменения доступны три пути: остаться на Rust 1.98, обновить зависимости или явно указать тип возврата в конкретном месте кода

Почему это важно

Never-тип долго был внутренней возможностью компилятора Rust, и его стабилизация, редкий случай, когда языковая механика меняется намеренно ценой небольшой обратной несовместимости. Практически он позволяет писать более эффективный обобщённый код: если реализация трейта вроде FromStr гарантированно не может завершиться ошибкой, тип ошибки можно объявить как never-тип, и компилятор выкинет весь код обработки этой ошибки. Теоретически он упрощает вывод типов для конструкций вроде бесконечных циклов, для которых иначе пришлось бы придумывать особые правила.

Кому это важно

В первую очередь, разработчикам на Rust, которые пишут обобщённый (generic) код и библиотеки с потенциально бесповторяющимися (infallible) реализациями трейтов, а также авторам и мейнтейнерам популярных крейтов на crates.io, чей код проверялся прогоном crater и часть которого потребовала бэкпорта исправлений. Косвенно это касается и мейнтейнеров самого компилятора Rust как пример процесса безопасного проведения обратно несовместимых изменений через язык с гарантиями стабильности.

Как это применить

Разработчику, использующему стандартный тип Infallible или обобщённые функции с типом возврата, который компилятор выводит неявно, стоит проверить код на Rust 1.99: если сборка ломается, есть три пути, остаться на версии 1.98, обновить зависимости до версий с исправлением или явно указать нужный тип в месте вызова (например, foo::<()>()? вместо foo()?). Авторам библиотек, где ошибка гарантированно не встречается, стабилизация даёт возможность объявить type Err = !, чтобы компилятор убрал лишний код обработки несуществующей ошибки.

Можно ли доверять

Материал написан для LWN, специализированного технического издания о разработке Linux и открытого ПО с историей точных инженерных разборов, и опирается на конкретные цифры прогона инструмента crater, а не на пересказ анонса. На Hacker News обсуждение набрало 224 балла и 81 комментарий, что для профильной языковой темы говорит о заметном интересе технической аудитории.

Риски и подводные камни

Изменение всё же обратно несовместимо: даже после бэкпортов часть кода, зависящего от старых версий библиотек, перестанет собираться на Rust 1.99, и часть авторов библиотек отказалась делать бэкпорт для версий, которые считают снятыми с поддержки. Разработчикам, которые не отслеживали предупреждения компилятора с 2024 года, стоит заранее прогнать сборку на новой версии, а не обновляться вслепую.