Generic Const Args (GCA) — набор фич, расширяющих язык так, что в позиции константного аргументааргумент обобщённого типа, который передаётся в угловых скобках и должен быть известен на этапе компиляции становится допустимо строить массивы, кортежи и ADTалгебраический тип данных: структура или перечисление. Разбираем, что именно добавляет каждая фича, как компилятор разбирает такие аргументы и какие ограничения остаются.
Что добавляют GCA поверх adt_const_params
Фича adt_const_params позволяет лишь объявить generic-параметрпараметр обобщения, подставляемый при мономорфизации вида const N: Foo. Она не даёт строить значения в позиции аргумента. Построение — это уже новая функциональность gca_adts: она вводит возможность конструировать ADT в константном аргументе. Новые поддерживаемые выражения записываются внутри вызова макроса gca!, который помечает выражение как константный аргумент нового вида: только внутри такого вызова допустимы конструкции, использующие generic-параметры, — без него запись Bar<{ Foo { field: N } }> привела бы к ошибке компилятора.
Поддержка массивов, кортежей и ADT объединена в одну фичу gca_adts. Построение перечислений и структур работает независимо от синтаксиса их определения — unit, tuple или struct. Например, accepts::<gca!(TupleStruct(N))>(); строит tuple-структуру, а accepts::<gca!(MyEnum::Record { x: N })>(); — вариант перечисления.
Ограничение: gca!(..)-аргументы поддерживают меньше видов выражений, чем обычные константные аргументы. Арифметика и вызовы функций внутри gca! не поддерживаются. Обойти это при одновременном использовании generic-параметров позволяет фича gca_const_items.
Зачем нужна явная обёртка gca!(..)
Все новые поддерживаемые выражения GCA должны быть записаны внутри вызова макроса gca!. Причина — в синтаксической неоднозначности. Рассмотрим пару ::<Bar(ONE)> и ::<Foo(N)>: они полностью синтаксически эквивалентны, но Foo(N) обязано быть gca!(..)-константным аргументом, потому что содержит путь к generic-параметру N, а Bar(ONE) не должно им быть, потому что это вызов функции, не поддерживаемый в gca!(..)-аргументах.
Без gca!(..) нет чёткого различия между константными аргументами, которые могут использовать generic-параметры, и теми, которые не могут, — это «muddies the waters about what Const Arguments are accepted». Поэтому обработка вынесена в отдельные feature-гейты, которые неявно подставляют макрос gca!(..) в разных позициях, избавляя от необходимости писать его вручную. Эти фичи очень экспериментальны и работают не особенно хорошо.
Как компилятор разбирает const-аргументы
Разбор generic-аргументов сегмента пути
При разборе generic-аргументов сегмента пути компилятор превращает их в своё внутреннее представление: он принимает span, def_id и сегмент пути, передаёт основную работу дальше, подставляя пустой список родительских аргументов и отсутствие self-типа, а затем проверяет первый constraint сегмента и при его наличии запрещает ассоциированное ограничение.
Различие между видами аргументов происходит не здесь, а при определении вида аргумента, где по тегу указателя выбирается один из трёх вариантов: регион, тип или константа. Соответственно, извлечение региона, типа и константы даёт результат только для своего варианта, а преобразование в term переводит тип или константу в ty::Term.
При обработке Self вид аргумента учитывается: если параметр имеет типовой вид, в аргументы кладётся self-тип или выведенный аргумент. Если встречается тип или константа там, где ожидается lifetime, lifetimes выводятся, а вызвавший это тип или константа запоминается — для последующей диагностики.
Литералы против всего остального
При понижении анонимной константы сначала вычисляется её тип, а затем она пытается понизиться как литерал. Если литерал распознан, его значение используется напрямую; иначе создаётся алиаспсевдоним константы, вычисление которой отложено.
Литералом считается только сам литерал и унарное отрицание литерала; блок без стейтментов разворачивается, чтобы { 1 } распознавалось как литерал, а всё остальное литералом не считается. Затем проверяется соответствие типа литерала ожидаемому типу, и при совпадении создаётся значение константы.
Смысл такого разделения — в комментарии: «Literals are eagerly converted to a constant, everything else becomes ConstKind::Alias». Литерал даёт значение константы сразу, без отложенного вычисления через алиас.
Массивы, кортежи и структуры
При понижении массива проверяется, что ожидаемый тип является массивом; если нет — выдаётся ошибка «expected {ty}, found const array». Иначе каждый элемент понижается с типом элемента, а тип valtreeвнутреннее представление значения константы в компиляторе строится по синтаксису, а не по ожидаемому типу: «construct the resulting valtree's type based on the provided syntax rather than the expected type».
При понижении кортежа проверяется, что ожидаемый тип является кортежем; если нет — выдаётся ошибка «expected {}, found {}» с найденным фрагментом; та же ошибка — при несовпадении числа выражений и типов.
При понижении структуры путь должен разрешаться в вариант или структуру, иначе выдаётся «struct expression with invalid base path». Для tuple-варианта конструктора действует ограничение: «Only an enum can host a tuple-variant constructor (<Option<u32>>::Some(..)).» Для прочих self-типов типо-относительный вызов является ассоциированной функцией и должен быть обёрнут в const { ... }.
Восстановление после вызова функции в позиции аргумента
Восстановление срабатывает при понижении const-аргумента вида вызова с типо-относительным путём — то есть когда пользователь написал что-то вроде Type::Name(...) там, где ожидался константный аргумент. Проверяется, разрешается ли self-тип в структуру, объединение, внешний тип или примитив: для таких типов типо-относительный вызов не может быть конструктором варианта, а является ассоциированной функцией, которую нужно обернуть в const { ... }. При срабатывании выдаётся ошибка ComplexConstArg, и понижение немедленно даёт константу-ошибку.
Перечисления, алиасы, Self и параметры типа пропускаются: каждый из них может разрешиться в перечисление и должен дойти до понижения конструктора. Проверка выполняется до понижения self-типа, чтобы избежать ложной ошибки E0107 «missing generics» для generic-структур без аргументов и непрозрачных ошибок разрешения для примитивов и внешних типов.
Проверка количества и типов generic-аргументов
Подсчёт аргументов
check_generic_arg_count_for_value_path — обёртка для проверки числа generic-аргументов при вызове функции: она формирует позицию «значение» и делегирует работу в check_generic_arg_count, передавая признак наличия Self. check_generic_arg_count сверяет, сколько generic-аргументов передано в сегменте пути, с тем, сколько их ожидается у соответствующего определения, и решает, укладывается ли переданное число в допустимый диапазон. Границы этого диапазона — expected_min и expected_max: минимальное и максимальное число аргументов, которое определение готово принять. Минимум задаёт, сколько аргументов обязаны быть указаны явно, а максимум — сколько их вообще допустимо с учётом параметров, имеющих значения по умолчанию.
Внутри берутся аргументы сегмента, default-значения и счётчики параметров. Синтетические параметры, порождённые impl Trait, вычитаются из числа именованных типовых параметров: synth_type_param_count считается по признаку synthetic: true, а затем named_type_param_count = param_counts.types - has_self as usize - synth_type_param_count.
Default-значения учитываются через разницу num_default_params = expected_max - expected_min, где допустимым считается диапазон expected_min..=expected_max. Для лайфтаймов минимум равен нулю, если лайфтаймы выводятся, иначе числу параметров-лайфтаймов, а максимум — числу параметров-лайфтаймов. При выходе за диапазон формируется признак избытка или недостатка лайфтаймов и эмитится WrongNumberOfGenericArgs. Если предоставлено больше, чем максимум, дополнительно проверяется, не являются ли лишние аргументы синтетическими вроде impl Trait.
Диагностика несовпадения
generic_arg_mismatch_err создаёт ошибку E0747 с сообщением вида «{arg.descr()} provided when a {param.kind.descr()} was expected» — то есть сообщает, какой вид аргумента передан вместо ожидаемого вида параметра. Затем добавляются специфичные подсказки: при передаче типа там, где ожидалась константа, предлагается обернуть аргумент в фигурные скобки; при передаче массива вместо usize добавляется метка «array type provided where a usize was expected» и предложение использовать значение длины массива.
В конце emit_err сравнивает порядок вида параметра и вида аргумента и, если возможна ошибка порядка и порядки не равны, добавляет примечание «{first} arguments must be provided before {last} arguments». Комментарий «This note is only true when generic parameters are strictly ordered by their kind» относится именно к этому примечанию: оно корректно только тогда, когда generic-параметры строго упорядочены по своему виду, поскольку сравнение опирается на этот порядок. Флаг возможной ошибки порядка вычисляется как проверка, что оставшиеся аргументы не отсортированы по своему виду.
Продолжение после ошибки
После выдачи ошибки несоответствия mk_args не пытается сопоставлять оставшиеся аргументы с параметрами: он пропускает все оставшиеся аргументы, чтобы ошибка не «всплывала» и не порождала дополнительные, нерелевантные диагностики. Вместо некорректного аргумента ничего не подставляется — аргумент просто игнорируется, а остальные параметры выводятся. Когда аргументы закончились, а параметры ещё есть, для каждого оставшегося параметра добавляется выведенный аргумент.
Const items в типовой системе
gca_const_items против gca_min_const_items
типовая системачасть компилятора, которая проверяет и сравнивает типы; константа попадает в неё, когда её указывают аргументом типа
const itemименованная константа, используемая как аргумент типа — это значение, которое компилятор должен уметь сравнивать и подставлять на этапе проверки типов, а не только при выполнении программы. Именно поэтому вопрос о том, какие const items допустимы в типовой системе, важен: чем больше выражений туда попадает, тем сложнее гарантировать, что две константы считаются равными предсказуемо.
gca_const_items позволяет использовать в типовой системе произвольные const items и дополнительно требует нестабильный флаг -Znext-solver.
gca_min_const_items — более узкая фича: она допускает в типовой системе только те const items, которые определены через gca!(..). Например, GOOD<const N: usize>: usize = gca!(N); разрешён, а BAD<const N: usize>: usize = N; — нет. Проверка выполняется в check_const_item_in_type_system: если включена gca_const_items() или константа является «прямой», использование разрешено, иначе выдаётся ошибка «use of const in the type system not marked as direct».
Требование gca!(..) ограничивает выразительность: это исключает аспекты gca_const_items, вызывающие post-mono ошибкиошибки, которые проявляются только после мономорфизации, то есть при подстановке конкретных типов в обобщённый код, и даёт простые правила определения равенства двух констант без неожиданностей. Кроме того, gca_min_const_items значительно проще реализовать, поэтому её можно стабилизировать раньше, чем полную версию.
Атрибут #[rustc_always_gca]
Для ассоциированных констант трейтов существует атрибут #[rustc_always_gca], который обязывает реализовывать ассоциированную константу всегда как выражение gca!(..). В примере без макролесс-режима const ASSOC: usize = N; помечено как «Not OK! not a gca!(..) expression», а const ASSOC: usize = gca!(N); — как «OK!». При включённом gca_macroless_items реализация const ASSOC: usize = N; становится допустимой без явного gca!(..), поскольку макрос добавляется неявно.
Почему пример с make_dyn не компилируется на стабильном Rust
Пример с make_dyn<const N: usize, T: Trait<ASSOC = N>> падает по двум причинам. Во-первых, в отличие от associated types, не поддерживается ограничение associated constants через bound вида T: Trait<ASSOC = { N }>. Во-вторых, в отличие от associated types, не разрешается создавать dyn-типы для трейтов, у которых есть associated consts. Оба механизма включаются флагом gca_const_items.
Использование const item в типе относится к gca_const_items, которая дополнительно требует -Znext-solver. Это позволяет произвольным выражениям косвенно попадать в систему типов: gca!(N + 1) напрямую использовать нельзя, но const item, определённый как const FOO: usize = N + 1;, — можно. Минимальная версия gca_min_const_items поддерживает почти то же самое, но только для const items, определённых как gca!(..), и не требует -Znext-solver.
Диагностика check_const_item_in_type_system
Функция принимает вид псевдонима константы, используемой в типовой системе, и span, и возвращает результат проверки. Она пропускает проверку без ошибки, если включена фича gca_const_items или константа является «прямой». Иначе создаётся ошибка «use of const in the type system not marked as direct».
Затем функция пытается получить HIR-узел по def_id для всех вариантов вида псевдонима. Если у узла есть тело, предлагается многочастная правка, оборачивающая тело в core::gca!(...). Если тела нет и это проекция, предлагается добавить атрибут #[rustc_always_gca]. Если узел не локальный, добавляется примечание «only consts with a gca! right-hand side may be used in types».
Ограничения и пограничные случаи
Что пока не поддерживается
Array repeat expressions (выражения повторения массива, например [N1; 2]) сейчас не поддерживаются, но их планируют поддержать. Вызов accepts_arrays::<gca!([N1; 2])>() помечен комментарием «currently disallowed :(».
В коде есть несколько FIXME, фиксирующих неполноту реализации: в lower_const_arg_struct — о необходимости дедупликации с логикой HIR typeck; при обходе полей структуры — о том, что privacy, stability и macro hygiene не обрабатываются; для путей к функциям в const-аргументах — о намерении создать настоящие константы, чтобы fn items стали допустимы как const paths (сами function items сейчас отклоняются с ошибкой «function items cannot be used as const args»); в lower_const_arg_anon — о необходимости использовать правильные generic args вместо instantiate_identity().
Запрет generic-параметров в анонимных константах
Запрет реализован через anon_const_forbids_generic_params, который возвращает контекст запрета для текущего анонимного const, если он запрещает использование generic-параметров, и None иначе. Для этого берётся корневой def_id и проверяется, что это анонимная константа; затем по виду анонимной константы выбирается контекст: для константного аргумента — ConstArgument, для дискриминанта перечисления при нулевом числе generic-параметров — EnumDiscriminant, для остальных видов — None.
check_param_res_if_mcg_for_instantiate_value_path применяется к результату разрешения: если это const-параметр и контекст запрета есть, создаётся обходчик и возвращается ошибка. check_param_uses_if_mcg сначала проверяет наличие контекста и что терм содержит параметры или escaping bound vars, затем прогоняет обходчик.
Сам ForbidParamUsesFolder в fold_ty при обнаружении параметра типа, в fold_const при обнаружении параметра-константы и в fold_region при обнаружении early- или late-параметра региона возвращает ошибку через self.error().
Тексты диагностик зависят от контекста и от того, является ли алиас Self:
- для дискриминанта перечисления с
Self— «genericSelftypes are not permitted in enum discriminant values», иначе «generic parameters may not be used in enum discriminant values»; - для константного аргумента с
Self— «genericSelftypes are currently not permitted in anonymous constants»; - для константного аргумента без
Selfпри включённойgca_const_items— «generic parameters in const blocks are not allowed; use a namedconstitem instead», иначе «generic parameters may not be used in const operations».
Взаимодействие с существующими механизмами типовой системы
Раскрытие абстрактных констант
is_const_evaluatableпроверка, можно ли вычислить константу на этапе проверки типов: от её результата зависит, разрешено ли использовать эту константу в типовой системе сначала раскрывает абстрактные константы и смотрит на вид полученной константы: для алиаса и выражения проверка продолжается, для параметра, bound-переменной, placeholder, значения и ошибки сразу возвращается успех, для инференс-переменной — ошибка о том, что константа упоминает инференс.
При включённой generic_const_exprs константа снова раскрывается и проверяется, является ли она анонимной. Если нет, она ищется в окружении параметров; при неудаче возвращается ошибка о упоминании инференс- или параметр-переменных. Если после этого исходная константа имеет вид выражения, то, поскольку вычисление выражений ещё не реализовано, вызывается аварийное завершение с сообщением «evaluating ConstKind::Expr is not currently supported». Для алиаса вместо этого выполняется попытка вычисления константы, и по результату возвращается успех либо соответствующая ошибка.
Представление аргументов
GenericArg — это интернированный указатель, в младших 2 битах которого хранится тег, указывающий на вид сущности: регион, тип или константа. Вид извлекается методом kind, который по тегу возвращает соответствующий вариант. Список аргументов — это список GenericArg.
extend_with_error дополняет исходный список до полного числа аргументов, ожидаемых def_id: для каждого параметра берётся аргумент по его индексу, а если аргумента нет — подставляется ошибка для этого параметра. Заполнение идёт по параметрам с проверкой соответствия индекса и длины.
Переиспользование аргументов родителя
rebase_onto строит аргументы для дочернего элемента, беря все аргументы целевого родителя и добавляя к ним только те аргументы исходного элемента, которые идут после параметров исходного родителя. Для этого он получает число параметров исходного родителя и пропускает ровно столько первых аргументов исходного списка.
Инвариант: результирующий список начинается с аргументов целевого родителя, а затем содержит «хвост» исходных аргументов, начиная с позиции числа параметров родителя. Пример из документации: для trait X<S> { fn f<T>(); } и impl<U> X<U> for U { fn f<V>() {} } при self = [Self, S, T], source_ancestor = trait, target_args = [U] результат — [U, T].
type_at, region_at и const_at не переиспользуют аргументы родителя, а лишь извлекают конкретный аргумент по индексу и проверяют, что его вид соответствует ожидаемому — тип, регион или константа, — иначе вызывают bug!.
impl Trait и элидированные лайфтаймы в путях
В path.rs impl Trait разрешён только в последнем сегменте пути: замыкание, определяющее контекст, возвращает исходный контекст только для последнего сегмента, иначе — запрет с позицией «путь». Это применяется и при обходе сегментов до проекции, и при обходе «расширений» после базового пути.
maybe_insert_elided_lifetimes_in_path вставляет элидированные лайфтаймы, потому что резолвер уже зафиксировал для сегмента диапазон элидированных лайфтаймов, и функция вычисляет их количество как разность границ диапазона. Место вставки выбирается так, чтобы диагностика могла указать, где лайфтайм не может быть выведен: при отсутствии скобок берётся пустой спан сразу после идентификатора, а при наличии скобок — пустой спан сразу после <. Сами вставленные лайфтаймы создаются отдельным методом и добавляются в начало списка аргументов.
Что из этого следует на практике
- Обёртка
gca!(..)обязательна для всех новых выражений: компилятор не может отличитьFoo(N)от вызова функцииBar(ONE)без неё, поэтому макрос — единственный способ пометить аргумент как «может использовать generic-параметры». - Внутри
gca!нет арифметики и вызовов функций. Если выражение нужно, его выносят в именованную константу и используют черезgca_const_items(требует-Znext-solver) илиgca_min_const_items(не требует, но принимает только правые части черезgca!(..)). gca_min_const_itemsстабилизируется раньше полной версии: она проще в реализации, даёт предсказуемые правила равенства констант и не порождает post-mono ошибок — ценой выразительности.- Array repeat expressions и function items как константные аргументы пока не работают; для fn items ожидается появление настоящих констант.
- Диагностики при неверном числе или виде аргументов устроены так, чтобы не порождать каскад: после первой ошибки оставшиеся аргументы игнорируются, а параметры выводятся. Примечание о порядке аргументов появляется только тогда, когда параметры строго упорядочены по виду.
Где смотреть в коде
- mod.rs: check_const_item_in_type_system
- mod.rs: fold_const
- mod.rs: error
- mod.rs: try_recover_misrepresented_function_call
- mod.rs: lower_const_arg_anon
- generic_args.rs: type_at
- const_evaluatable.rs: expand_abstract_consts
- generic_args.rs: kind