Привет, Хабр!Когда вы пишете вот такую строчку:const N: u64 = fib(50);внутри компилятора происходит странное. rustc не генерирует машинный код для fib, не зовёт процессор и не подставляет ответ откуда-то сбоку. Он берёт вашу функцию, разворачивает её в промежуточное представление и исполняет шаг за шагом прямо у себя внутри, на маленькой виртуальной машине. И вот вопрос, на который мало кто может ответить с ходу: а кто конкретно это исполняет? Где живёт тот интерпретатор? Почему 255 + 1 в const падает с ошибкой компиляции, а в рантайме просто паникует? Почему можно посчитать таблицу из тысячи элементов циклом, но нельзя написать if a < b для дженерика? И почему 0.0 / 0.0 в const официально разрешили вести себя недетерминированно?Погнали разбираться. Читать далее

Привет, Хабр!
Когда вы пишете вот такую строчку:
const N: u64 = fib(50);внутри компилятора происходит странное. rustc не генерирует машинный код для fib, не зовёт процессор и не подставляет ответ откуда-то сбоку. Он берёт вашу функцию, разворачивает её в промежуточное представление и исполняет шаг за шагом прямо у себя внутри, на маленькой виртуальной машине.
И вот вопрос, на который мало кто может ответить с ходу: а кто конкретно это исполняет? Где живёт тот интерпретатор? Почему 255 + 1 в const падает с ошибкой компиляции, а в рантайме просто паникует? Почему можно посчитать таблицу из тысячи элементов циклом, но нельзя написать if a < b для дженерика? И почему 0.0 / 0.0 в const официально разрешили вести себя недетерминированно?
Погнали разбираться.
Сначала про контекст, иначе всё развалитсяПоловина путаницы вокруг const fn растёт из непонимания, что такое const-контекст.
В Rust есть места, где значение обязано быть известно компилятору. Их немного, и вот они все:
const SIZE: usize = 4 * 8;
let buf = [0u8; SIZE]; // длина массива
enum Flags { A = 1 << 0, B = 1 << 1 } // дискриминант варианта enum
struct Matrix<const N: usize>; // аргумент const-дженерика
static TABLE: [u32; 256] = build(); // инициализатор const и static
let x = const { SIZE * 2 }; // инлайн const-блок, с 1.79Это и есть const-контекст. Внутри него разрешены не любые выражения, а только те, что компилятор гарантированно посчитает сам.
Пометка const на функции ничего не меняет для обычных вызовов:
const fn square(x: u64) -> u64 { x * x }
const NINE: u64 = square(3); // посчитано на этапе компиляции
fn main() {
let n = read_number(); // рантайм-значение
println!("{}", square(n)); // та же функция, обычный рантайм-вызов
}Одна и та же square. В первом случае её исполняет компилятор, во втором она компилируется в машинный код и работает как все остальные функции. const это не «функция работает на этапе компиляции», а «функцию можно позвать из const-контекста, и за это она соглашается на ограничения».
Внутри rustc живёт интерпретатор.
Не оптимизатор, не генератор кода, а именно интерпретатор: виртуальная машина, которая выполняет промежуточное представление среднего уровня (MIR, Mid-level Intermediate Representation) напрямую, ничего не компилируя. Когда компилятору нужна константа, он не идёт в процессор, а скармливает MIR этой машине и ждёт результат.
Это тот же самый движок, что и Miri, инструмент для отлова неопределённого поведения (UB, undefined behavior). Народ думает, что Miri и вычисление констант это разные штуки. Нет. У них общая виртуальная машина в rustc_const_eval, а отличия в поведении вынесены в трейт Machine, который для каждого режима реализован по-своему:
// сильно упрощённо, изнутри rustc
pub trait Machine<'tcx> {
type MemoryKind;
// ... десятки методов: как звать функции, как трогать память,
// что считать ошибкой, разрешён ли доступ к ОС, и так далее
}Const-evaluator это, грубо говоря, Miri с урезанными правами. Ему запрещено дёргать настоящие системные вызовы, открывать файлы, звать функции из C.
Биография у движка занятная. Miri начинался в 2015 году как студенческий исследовательский проект @solson. В 2016-м к нему подключился @oli-obk и довёл до состояния, в котором интерпретатор можно встроить в компилятор на роль const-evaluator, заменив старый движок, ходивший прямо по абстрактному синтаксическому дереву (AST). С тех пор разработчики Miri и авторы движка const-eval это во многом одни и те же люди.
Снаружи всё это дёргается через семейство запросов tcx.const_eval_* и работает лениво. Пока константа никому не нужна, её не считают. Когда понадобилась, движок входит в цикл и крутит метод step (он лежит в rustc_const_eval/src/interpret/step.rs), выполняя MIR-инструкции одну за другой, пока не дойдёт до результата или не упрётся в операцию, которую не умеет. Тогда всё останавливается с ошибкой компиляции. Результат кэшируется, так что дважды одно и то же не пересчитывается.
У константы внутри компилятора два представления. Для системы типов (например, чтобы сравнить два аргумента const-дженерика на равенство) результат гонят в valtree, структурированное дерево значений, которое можно осмысленно разобрать. А для генерации кода тот же результат живёт как ConstValue, низкоуровневый блоб в памяти. Запросы так и разведены: const_eval_global_id_for_typeck отдаёт valtree, const_eval_global_id отдаёт ConstValue.
Чтобы было видно, что именно исполняет интерпретатор, посмотрим на MIR. Возьмём простейшее:
const fn add(a: u32, b: u32) -> u32 {
a + b
}В MIR это разворачивается примерно так (упрощённо, точный вид зависит от версии rustc):
fn add(_1: u32, _2: u32) -> u32 {
let mut _0: u32; // возвращаемое значение
let mut _3: (u32, bool); // (результат, флаг переполнения)
bb0: {
_3 = AddWithOverflow(copy _1, copy _2);
assert(!move (_3.1: bool), "attempt to add with overflow") -> bb1;
}
bb1: {
_0 = move (_3.0: u32);
return;
}
}Видно главное. MIR это граф из базовых блоков (bb0, bb1), переходы между которыми явные. Сложение это не просто +, а вычисление пары (результат, было_ли_переполнение) и явная проверка assert. Интерпретатор просто идёт по этим блокам: посчитал rvalue, проверил assert, записал в локальную переменную, перешёл дальше.
И вот тут вылезает первое отличие от рантайма. Если переполнение случится при вычислении константы, assert не паникнет в рантайме, а станет ошибкой компиляции:
const Y: u8 = 200 + 100;
// error: this arithmetic operation will overflowВ рантайме то же сложение в debug-сборке паникнет, а в release тихо завернётся по модулю. В const-контексте у вас нет рантайма, в котором можно паниковать, поэтому компилятор обязан поймать это здесь и сейчас.
Память, которой нетЧтобы ловить UB, интерпретатор не имеет права представлять память так, как она лежит в железе. Адрес в его модели это не просто число.
Результатлом вычисления будетConstValue, и у него несколько форм:
// упрощённая суть, не дословно
enum ConstValue {
Scalar(Scalar), // одно скалярное значение
Slice { .. }, // байтовый срез или строка
Indirect { .. }, // ссылка на виртуальную аллокацию
}
enum Scalar {
Int(..), // сырое целое
Ptr(AllocId, Provenance), // указатель: в какую аллокацию смотрит
}Обратите внимание на Ptr. Указатель тащит с собой происхождение (provenance): информацию о том, в какую аллокацию он показывает. Память здесь не плоский массив байтов, адресуемый числами, а набор отдельных аллокаций со своей структурой.
Из-за этого интерпретатор видит то, что в рантайме прошло бы незаметно. Вышли за границу куска памяти? Это не «чтение мусора», а ошибка компиляции:
const A: [i32; 3] = [1, 2, 3];
const X: i32 = A[5];
// error[E0080]: evaluation of constant value failed
// index out of bounds: the length is 3 but the index is 5Попытались превратить указатель в число и обратно? В const это всегда UB, ещё с тех пор как движок научился transmute и объединениям. Причина в модели: у указателя есть происхождение, у целого числа его нет, обратное преобразование теряет эту информацию и ломает абстрактную машину. Поэтому операции с сырыми указателями в const жёстко ограничены.
У всего этого есть имя, const-безопасность (const safety). Безопасный код в const-контексте обязан гарантированно не упереться в ошибку вычисления. Паниковать ему можно, как и обычному коду, а вот наткнуться на операцию, которую движок физически не умеет посчитать, нет. Система типов отсекает такое заранее.
&mut в const и промоушен константДолго в const почти нельзя было ничего менять по ссылке. Никаких &mut внутри вычисления. И ограничение это не техническое, а смысловое.
Константа обязана оставаться константой: её значение и её смысл как образца при сопоставлении должны быть одинаковыми на всём протяжении работы программы. Узел развязывали постепенно, и в Rust 1.83 наконец застабилизировали &mut, mut, &Cell и const Cell в const-контексте. Теперь так можно:
const fn doubled() -> [i32; 3] {
let mut a = [1, 2, 3];
let r = &mut a[0]; // ок с 1.83: меняем по &mut прямо в вычислении
*r *= 2;
a
}
const RESULT: [i32; 3] = doubled(); // [2, 2, 3]Но с важной оговоркой: &mut это рабочий инструмент внутри вычисления. Протащить мутабельную ссылку в финальное значение константы нельзя:
const BAD: &mut i32 = &mut 4;
// error[E0764]: mutable references are not allowed
// in the final value of constantsЛогика та же, что и со статиками: ссылаться на static в const разрешили, а читать значение мутабельного статика по-прежнему нельзя, иначе константа зависела бы от изменчивого состояния.
Заодно стоит знать про близкий механизм, промоушен констант. Некоторые выражения за & компилятор втихаря превращает в константы и выдаёт им 'static:
let x: &'static i32 = &42;
// &42 промоутится в анонимную константу, поэтому ссылка живёт 'staticЭто работает ровно потому, что под капотом есть const-evaluator, который умеет посчитать 42 на этапе компиляции и положить в статическую память.
История const fn это десятилетие медленного расширения. По вехам, чтобы был виден масштаб.
Стартовало в 1.31 (2018), и умела она тогда совсем мало: базовая арифметика, ноль ветвлений и циклов. К 1.46 (2020) завезли управляющие конструкции, и вот после этого в const стало можно писать настоящие алгоритмы:
const fn build_squares() -> [u32; 16] {
let mut t = [0u32; 16];
let mut i = 0;
while i < 16 { // циклы в const, с 1.46
t[i] = (i * i) as u32;
i += 1;
}
t
}
static SQUARES: [u32; 16] = build_squares(); // посчитано на этапе компиляцииПараллельно подъехали const-дженерики (min_const_generics в 1.51), а в 1.57 разрешили panic! в const, что открыло дорогу проверкам инвариантов прямо при компиляции:
const fn checked_cap(n: usize) -> usize {
assert!(n.is_power_of_two(), "ёмкость должна быть степенью двойки");
n
}
const CAP: usize = checked_cap(64); // ок
// const BAD: usize = checked_cap(63); // ошибка компиляции, не рантаймаДальше темп только рос. 1.79 (2024) дал инлайн-блоки const { ... }: можно явно войти в const-контекст посреди выражения, без отдельного объявления, причём с выводом типа и доступом к дженерикам из области видимости:
fn check<T>() {
const { assert!(size_of::<T>() > 0, "ZST сюда нельзя") };
// ...
}1.82 (октябрь 2024) разрешил арифметику с плавающей точкой, к ней вернёмся через абзац. 1.83 (ноябрь 2024) принёс мутабельные ссылки. И всё это время стандартная библиотека планомерно метила свои функции как const: методы срезов, строк, целочисленных типов, Option, Duration и так далее. К 1.85 (февраль 2025) подоспела редакция 2024.
Итог: к лету 2026 года на стабильном Rust в const можно делать много чего. Считать таблицы подстановок, валидировать конфиги, парсить байты, собирать хитрые константы циклами, временно меняя данные по &mut. Если код неполиморфный и не лезет в кучу, потолок высокий.
Звать методы трейтов в const по дженерикам на стабильном Rust нельзя до сих пор.
const fn min<T: Ord>(a: T, b: T) -> T {
if a < b { a } else { b }
}
// error[E0015]: cannot call non-const operator in constant functions
a < b это вызов метода трейта PartialOrd, а вызвать метод трейта по обобщённому параметру в const компилятор не даёт. По той же причине нет const-версии обобщённого ==, нет Option::map в const, нет много чего. Поэтому compile-time логику люди до сих пор уносят в build.rs или просто не пишут.
На nightly это уже работает, под фичей const_trait_impl, но синтаксис до сих пор переписывают, и именно эта возня держит фичу в экспериментальном статусе. Сейчас рабочий пример выглядит примерно так:
#![feature(const_trait_impl)]
#[const_trait]
trait Min {
fn min(self, other: Self) -> Self;
}
impl const Min for u32 {
fn min(self, other: Self) -> Self {
if self < other { self } else { other } // примитивный <, в const ок
}
}
const fn pick<T: [const] Min>(a: T, b: T) -> T {
a.min(b)
}
const M: u32 = pick(3, 7); // 3Трейт помечается как готовый к const (#[const_trait]). Реализация помечается impl const. А граница [const] Min означает «реализация обязана быть const, когда мы в const-контексте». Условную границу раньше писали ~const Trait, недавно заменили на [const] Trait, обсуждается и форма с ключевым словом const trait.
Компилятору пришлось завести понятие условий константности (const_conditions) и отдельный вид предикатов (HostEffectPredicate), которые ведут себя по-разному в зависимости от того, зовут функцию в рантайме или на этапе компиляции. Граница T: const Tr означает «всегда const» и проверяется как обычный предикат. Граница T: [const] Tr означает «const, только в const-контексте» и проверяется отдельно, через эти самые const_conditions.
Если попытаться скормить функции с const-границей тип без const-реализации, получите ошибку:
fn needs_const(_: impl const Min) {}
// needs_const(value_of_some_non_const_type)
// error[E0277]: the trait bound `T: const Min` is not satisfiedСама фича на nightly уже зрелая, часть стандартной библиотеки под неё переписана. Чего нет, так это принятого RFC на синтаксис и семантику. В планах Rust на 2026 год const-трейты стоят целью: дописать RFC, закрыть оставшиеся вопросы в компиляторе, вынести на публичное тестирование. Так что шанс увидеть их в стейбле оч приличный.
Чего нельзя и почему: NaN, куча, ввод-выводНачнём с истории, ради которой команде пришлось пойти на принципиальную уступку: числа с плавающей точкой.
Долго в const fn их можно было только копировать, но не считать. Держал это детерминизм. У const fn негласный контракт: посчитанная при компиляции, она даёт тот же результат, что и в рантайме. А с плавающей точкой это неправда. Стандарт IEEE 754 почти ничего не гарантирует про биты значения «не-число» (NaN), и одна и та же операция на одних входах может выдать разный NaN. Например, a b и b a, если оба NaN, на разном железе вполне дадут разные битовые представления. Выходит, 0.0 / 0.0 недетерминирован, при том что const C: f32 = 0.0 / 0.0; работает на стабильном Rust ещё с версии 1.0.
Решили это с помощью того, что Rust официально признал, что const fn может вести себя недетерминированно в рантайме и выдавать на этапе компиляции результат, зависящий от платформы, версии и флагов. Касается это битов NaN. После этого в 1.82 арифметику с плавающей точкой в const fn разблокировали:
const fn mix(a: f64, b: f64) -> f64 {
a * b + 1.0 // с 1.82 это ок
}Код не должен полагаться на то, что const fn всегда выдаёт строго одинаковый результат. Так что считать f64 в const теперь можно, но если в вычислении замешан NaN, его точные биты вам никто не обещает. И длины массивов от хитрых float-вычислений лучше не делать.
Дальше трансцендентные функции. Они в const не работают, но не из-за глубокого запрета, а просто потому, что в стандартной библиотеке пока не помечены как const fn:
const LOG2PI: f64 = (2.0 * std::f64::consts::PI).ln();
// error[E0015]: cannot call non-const fn `f64::ln` in constantsЭто вопрос времени.
И главное, чего нет совсем: куча. Выделить память на этапе компиляции нельзя, потому что за кучей стоит аллокатор рантайма, а его в компиляторе нет:
const fn make() -> Box<i32> {
Box::new(5)
}
// error[E0015]: cannot call non-const fn `Box::<i32>::new` in constant functionsПо той же причине нет Vec, String, динамической диспетчеризации через dyn Trait и нет async. На nightly, правда, уже есть низкоуровневые штучки выделения памяти в const (const_allocate, const_deallocate), но это не готовый Vec. Кучу в const, кстати, во многом блокируют именно const-трейты: без них нормальный Vec не сделать.
Если подытожить, картина складывается такая.
За десять лет const fn заметно повзрослела. На стабильном Rust к 2026 году внутри константных вычислений доступны управляющие конструкции, мутабельные ссылки, inline-блоки const {}, операции с плавающей точкой и целый набор const-методов в стандартной библиотеке — внешне почти обычный код, только исполняемый на этапе компиляции.
Главные пробелы по‑прежнему сводятся к двум словам: полиморфизм и куча. Методы трейтов в обобщённом контексте остаются за пределами стабильного канала; формулировку «const-трейты уже работают» стоит читать с уточнением «на nightly, со всеми оговорками, RFC ещё не закрыт». Выделение памяти в куче (Box, Vec и прочее) в const-контексте отсутствует фундаментально — по техническим причинам, и в ближайшей перспективе этого не изменится.
Для заранее известных данных и чистой арифметики инструмент вышел мощный. Как только возникает желание поднять dyn Trait или аллокации во время компиляции — мы пока вне игры. Остаётся следить за RFC, при необходимости сидеть на nightly и трезво оценивать границы применимого.
Размещайте облачную инфраструктуру и масштабируйте сервисы с надежным облачным провайдером Beget.
Эксклюзивно для читателей Хабра мы даем бонус 10% при первом пополнении.

| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Как понять, что делает Rust-компилятор: визуализация AST, MIR и LLVM IR | 0 | 10.8 | 15-07-2026 |
| 2 | [Перевод] Сокращаем длительность компиляции проекта на Rust c 30 до 2 минут — пример с 1000 крейтов | 0 | 8.18 | 09-07-2026 |
| 3 | История о том, как я написал компилятор Svelte на Rust | 0 | 11.83 | 02-08-2026 |
| 4 | Полностью нечестное сравнение std::expected в C++23 и core::result::Result в Rust | 0 | 7.76 | 09-07-2026 |
| 5 | Как на самом деле работает .await: пишем свой async-рантайм на Rust с нуля | 0 | 8.59 | 21-06-2026 |
| 6 | От boot-кода до shell: несколько месяцев разработки ОС на Rust с ИИ-агентами | 0 | 12.89 | 30-07-2026 |
| 7 | Если ссылки схлопываются, значит это кому‑то нужно | 0 | 8.26 | 30-07-2026 |
| 8 | Четыре f64 за одну инструкцию не делают вас быстрыми: как я векторизовал торговый движок на Rust и словил CI на лжи | 0 | 7.51 | 29-07-2026 |
| 9 | HashMap в Rust: SwissTable, SIMD по 16 байт за раз и RawTable, который от вас спрятали | 0 | 9.2 | 27-07-2026 |
| 10 | Баги на диком западе: топ-10 ошибок в C и C++ проектах за 2025 год | 0 | 8.06 | 30-12-2025 |