Rust: Проблема отслеживания для RFC 2342, "Разрешить` if` и `match` в константах"

Созданный на 18 мар. 2018  ·  83Комментарии  ·  Источник: rust-lang/rust

Это проблема отслеживания для RFC «Разрешить if и match в константах» (rust-lang / rfcs # 2342).

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

Шаги:

  • [x] Реализуйте RFC
  • [] Настроить документацию ( см. Инструкцию по кузнице )
  • [x] PR стабилизации ( см. инструкцию по кузнице )
  • [x] let привязки в константах, которые используют операции короткого замыкания && и || . Сейчас они рассматриваются как & и | внутри элементов const и static .

Нерешенные вопросы:

Никто

A-const-eval A-const-fn B-RFC-approved C-tracking-issue F-const_if_match T-lang disposition-merge finished-final-comment-period

Самый полезный комментарий

Теперь, когда # 64470 и # 63812 были объединены, все необходимые для этого инструменты существуют в компиляторе. Мне все еще нужно внести некоторые изменения в систему запросов в отношении квалификации const, чтобы убедиться, что при включенной этой функции она не будет излишне неэффективной. Мы добиваемся прогресса, и я считаю, что экспериментальная реализация этого будет доступна каждую ночь через недели, а не месяцы (известные последние слова: smile :).

Все 83 Комментарий

  1. добавить для него функцию ворот
  2. Терминаторы switch и switchInt в https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L347 должны иметь собственный код в в случае, если функция ворота активна
  3. вместо одного текущего базового блока (https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L328) должен быть некий контейнер со списком базовые блоки еще предстоит обработать.

@ oli-obk Это немного сложнее, потому что сложный поток управления означает, что необходимо использовать анализ потока данных. Мне нужно вернуться к @alexreg и выяснить, как интегрировать их изменения.

@eddyb Хорошей отправной точкой, вероятно, было бы взять мою ветку const-qualif (минус верхний коммит), перенастроить ее по мастеру (не будет весело), ​​а затем добавить аннотации к данным, верно?

Есть новости по этому поводу?

@ mark-im Увы, нет. Я думаю, что @eddyb действительно был очень занят, потому что я даже не мог пинговать его по IRC последние несколько недель, ха. К сожалению, моя ветка const-qualif даже не компилируется с тех пор, как я в последний раз перебазировал ее поверх master. (Хотя я не верю, что настаивал.)

thread 'main' panicked at 'assertion failed: position <= slice.len()', libserialize/leb128.rs:97:1
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Could not compile `rustc_llvm`.

Caused by:
  process didn't exit successfully: `/Users/alex/Software/rust/build/bootstrap/debug/rustc --crate-name build_script_build librustc_llvm/build.rs --error-format json --crate-type bin --emit=dep-info,link -C opt-level=2 -C metadata=74f2a810ad96be1d -C extra-filename=-74f2a810ad96be1d --out-dir /Users/alex/Software/rust/build/x86_64-apple-darwin/stage1-rustc/release/build/rustc_llvm-74f2a810ad96be1d -L dependency=/Users/alex/Software/rust/build/x86_64-apple-darwin/stage1-rustc/release/deps --extern build_helper=/Users/alex/Software/rust/build/x86_64-apple-darwin/stage1-rustc/release/deps/libbuild_helper-89aaac40d3077cd7.rlib --extern cc=/Users/alex/Software/rust/build/x86_64-apple-darwin/stage1-rustc/release/deps/libcc-ead7d4af4a69e776.rlib` (exit code: 101)
warning: build failed, waiting for other jobs to finish...
error: build failed
command did not execute successfully: "/Users/alex/Software/rust/build/x86_64-apple-darwin/stage0/bin/cargo" "build" "--target" "x86_64-apple-darwin" "-j" "8" "--release" "--manifest-path" "/Users/alex/Software/rust/src/librustc_trans/Cargo.toml" "--features" " jemalloc" "--message-format" "json"
expected success, got: exit code: 101
thread 'main' panicked at 'cargo must succeed', bootstrap/compile.rs:1085:9
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failed to run: /Users/alex/Software/rust/build/bootstrap/debug/bootstrap -i build

Ладно, как ни странно, только сегодня я снова перебазировал, и, похоже, сейчас все в порядке! Похоже, произошел регресс, и его только что исправили. Теперь все в @eddyb .

@alexreg Извините, я переключился на местный график сна и вижу, что вы пинговали меня, когда я просыпаюсь, но тогда вы весь день не в сети, когда я бодрствую (тьфу часовые пояса).
Стоит ли просто сделать пиар из твоей ветки? Я забыл, что мы должны были с этим делать?

@eddyb Все в порядке, хе. Вы должны ложиться спать пораньше, так как я обычно встаю с 20:00 по Гринвичу, но это все хорошо! :-)

Мне очень жаль, мне потребовалось время, чтобы понять, что данная серия исправлений требует удаления Qualif::STATIC{,_REF} , то есть ошибок доступа к статике во время компиляции. OTOH, это уже нарушено с точки зрения const fn s и доступа к static s:

#![feature(const_fn)]
const fn read<T: Copy>(x: &T) -> T { *x }
static FOO: u32 = read(&BAR);
static BAR: u32 = 5;
fn main() {
    println!("{}", FOO);
}

Это не обнаруживается статически, вместо этого miri жалуется, что "висячий указатель был разыменован" (что действительно должно говорить что-то о static s вместо "висящего указателя").

Поэтому я думаю, что чтение static s во время компиляции должно быть нормальным, но некоторые люди хотят, чтобы const fn был "чистым" (т.е. "ссылочно прозрачным" или около того) во время выполнения, что будет означать, что чтение const fn из-за ссылки, полученной в качестве аргумента , нормально, но const fn никогда не сможет получить ссылку на static из воздуха (включая от const s).

Я думаю, что тогда мы можем продолжать статически отрицать упоминание static s (даже если только использовать их ссылку) в const s, const fn и других постоянных контекстах (включая повышенные).
Но нам все еще нужно удалить хак STATIC_REF который позволяет static принимать ссылку из других static но (плохо пытается и не может) запретить чтение из-за этих ссылок. .

Нужен ли нам для этого RFC?

Звучит неплохо по сравнению с статикой. Сомневаюсь, что для этого нужен RFC, может быть, просто пробежка кратера, но тогда я, вероятно, не лучший, чтобы сказать.

Обратите внимание, что мы не будем ничего ограничивать, мы ослабим ограничение, которое уже нарушено.

О, я неправильно понял. Значит, константная оценка по-прежнему будет правильной, но не прозрачной по ссылкам?

В последнем абзаце описан ссылочно прозрачный подход (но мы потеряем это свойство, если начнем разрешать упоминание static s в const s и const fn s). Я не думаю, что разумность действительно обсуждалась.

Что ж, "висячий указатель" определенно звучит как проблема с надежностью, но я доверяю вам в этом!

"dangling pointer" - плохое сообщение об ошибке, это просто miri, запрещающий чтение из static s. Единственные постоянные контексты, которые могут даже ссылаться на static s, - это другие static s, поэтому мы могли бы «просто» разрешить эти чтения, поскольку весь этот код всегда выполняется один раз, во время компиляции.

(из IRC) Подводя итог, ссылочно прозрачный const fn может достигать только замороженных выделений, без прохождения аргументов, что означает, что const требует того же ограничения, а незамороженные выделения могут поступать только из static s.

Мне нравится сохранять ссылочную прозрачность, поэтому идея @eddyb звучит фантастически!

Да, я тоже за то, чтобы const fns была чистой.

Обратите внимание, что некоторые, казалось бы, безобидные планы могут нарушить ссылочную прозрачность, например:

let x = 0;
let non_deterministic = &x as *const _ as usize;
if non_deterministic.count_ones() % 2 == 0 {
    // do one thing
} else {
    // do a completely different thing
}

Это приведет к сбою с ошибкой miri во время компиляции, но будет недетерминированным во время выполнения (потому что мы не можем пометить этот адрес памяти как «абстрактный», как это делает miri).

РЕДАКТИРОВАТЬ : у @Centril была идея unsafe в const fn (что мы можем делать до тех пор, пока не стабилизируем const fn ) и заявляют, что их можно использовать только способами, которые miri допускает во время компиляции.
Например, вычитание двух указателей в один и тот же локальный объект должно быть нормальным (вы получаете относительное расстояние, которое зависит только от макета типа, индексов массива и т. Д.), Но форматирование адреса ссылки (через {:p} ) является неправильным использованием, поэтому fmt::Pointer::fmt нельзя пометить как const fn .
Также ни один из признаков Ord / Eq для необработанных указателей не может быть помечен как const (всякий раз, когда мы получаем возможность аннотировать их как таковые), потому что они безопасны. но операция unsafe в const fn .

Зависит от того, что вы подразумеваете под «безобидным» ... Я определенно вижу причину, по которой мы хотели бы запретить такое недетерминированное поведение.

Было бы замечательно, если бы работа над этим продолжалась.

@lachlansneff Он движется ... не так быстро, как хотелось бы, но работа ведется. На данный момент ждем https://github.com/rust-lang/rust/pull/51110 в качестве блокировщика.

@alexreg Ах, спасибо. Было бы очень полезно иметь возможность отмечать совпадение или if как const, даже если оно не находится в const fn.

какие-либо обновления статуса теперь, когда # 51110 объединен?

@programmerjake Я жду отзывов от @eddyb на https://github.com/rust-lang/rust/pull/52518, прежде чем его можно будет объединить (надеюсь, очень скоро). В последнее время он был очень занят (всегда пользовался большим спросом), но за последние несколько дней он вернулся к обзорам и тому подобному, так что я надеюсь. Я подозреваю, что после этого он потребует некоторой работы лично, поскольку добавить надлежащий анализ потока данных - сложное дело. Хотя посмотрим.

Где-то в списках TODO в первом посте (ах) его следует добавить, чтобы удалить текущий ужасный взлом, который переводит && и || в & и | внутри констант.

@RalfJung Разве эта часть старого константного eval не исчезла, теперь, когда MIRI CTFE на месте?

AFAIK мы делаем этот перевод где-то при понижении HIR, потому что у нас есть код в const_qualify который отклоняет SwitchInt терминаторы, которые в противном случае были бы сгенерированы || / && .

Кроме того, еще один момент: @ oli-obk где-то сказал (но я не могу найти где), что условные выражения как-то сложнее, чем можно было бы наивно подумать ... Это было «просто» об анализе изменчивости drop / internal?

было ли это «только» об анализе изменчивости капель / внутренней?

Сейчас я пытаюсь это прояснить. Вернусь к вам, когда у меня будет вся информация

Какой у этого статус? Нужна ли для этого рабочая сила или это заблокировано для решения какой-то проблемы?

@ mark-im Блокируется при реализации надлежащего анализа потока данных для квалификации const. @eddyb является наиболее осведомленным в этой области, и ранее он уже работал над этим. (Так было и у меня, но такая стагнация ...) Если у @eddyb все еще нет времени, возможно, @ oli-obk или @RalfJung скоро займутся этим. :-)

58403 - это небольшой шаг к квалификации на основе потока данных.

@eddyb вы упомянули о сохранении ссылочной прозрачности в const fn , что я считаю хорошей идеей. Что, если бы вы запретили использование указателей в const fn ? Таким образом, ваш предыдущий образец кода больше не будет компилироваться:

let x = 0;
// compile time error: cannot cast reference to pointer in `const fun`
let non_deterministic = &x as *const _ as usize;
if non_deterministic.count_ones() % 2 == 0 {
    // do one thing
} else {
    // do a completely different thing
}

Ссылки по-прежнему будут разрешены, но вы не сможете их анализировать:

let x = 0;
let p = &x;
if *p != 0 {  // this is fine
    // do one thing
} else {
    // do a completely different thing
}

Дайте мне знать, если я полностью ошибаюсь, я просто подумал, что это хороший способ сделать это детерминированным.

@ jyn514, который уже покрыт нестабильными преобразованиями as usize (https://github.com/rust-lang/rust/issues/51910), но пользователи также могут сравнивать необработанные указатели (https://github.com/rust- lang / rust / issues / 53020), что так же плохо и, следовательно, также нестабильно. Мы можем справиться с этим независимо от потока управления.

Есть что-нибудь новенькое по этому поводу?

Есть обсуждение https://rust-lang.zulipchat.com/#narrow/stream/146212 -t-compiler.2Fconst-eval / topic / dataflow-based.20const.20qualification.20MVP

@ oli-obk твоя ссылка не работает. Что там написано?

У меня это работает ... вы должны войти в Zulip.

@alexreg хм, да, я полагаю, речь шла о квалификационной работе const на основе потока данных. @alexreg знаете, зачем он нужен для if и match в константах?

если у нас нет версии, основанной на потоке данных, мы либо случайно разрешаем &Cell<T> внутри констант, либо случайно запрещаем None::<&Cell<T>> (который работает на стабильной версии. Практически невозможно правильно реализовать без потока данных (или любая реализация будет быть плохой сломанной специальной версией потока данных)

@ est31 Ну, @ oli-obk понимает это намного лучше меня, но на высоком уровне практически все, что связано с ветвлением, будет предикатным анализом потока данных, если вам не нужна куча крайних случаев. В любом случае, похоже, что этот человек на Zulip пытается над этим работать, и, если нет, я знаю, что oli-obk и eddyb имеют намерения, может быть, в этом месяце или следующем (с того момента, когда я в последний раз говорил с ними об этом), хотя я могу Я не буду давать обещаний от их имени.

@alexreg @ mark-im @ est31 @ oli-obk Я должен быть в состоянии опубликовать мою WIP-реализацию квалификации const на основе потока данных где-то на этой неделе. Здесь есть много опасностей совместимости, поэтому фактическое слияние может занять некоторое время.

Супер; с нетерпением жду этого.

(копирование с # 57563 по запросу)

Можно ли использовать особый случай bool && bool , bool || bool и т. Д.? В настоящее время они могут выполняться в const fn , но для этого требуются побитовые операторы, что иногда нежелательно.

Они уже имеют особый регистр в элементах const и static - переводя их в побитовые операции. Но этот специальный кожух - огромная хитрость, и очень трудно убедиться, что это действительно правильно. Как вы сказали, это тоже иногда нежелательно. Так что мы не хотели бы делать это чаще.

На то, чтобы все делать правильно, потребуется немного времени, но это произойдет. Если мы навалим за это время слишком много хаков, мы можем загнать себя в угол, из которого мы не сможем выбраться (если некоторые из этих хаков в конечном итоге взаимодействуют неверным образом, тем самым случайно стабилизируя поведение, которого мы не хотим).

Теперь, когда # 64470 и # 63812 были объединены, все необходимые для этого инструменты существуют в компиляторе. Мне все еще нужно внести некоторые изменения в систему запросов в отношении квалификации const, чтобы убедиться, что при включенной этой функции она не будет излишне неэффективной. Мы добиваемся прогресса, и я считаю, что экспериментальная реализация этого будет доступна каждую ночь через недели, а не месяцы (известные последние слова: smile :).

@ ecstatic-morse Приятно слышать! Спасибо за ваши согласованные усилия, чтобы сделать это; Я лично давно увлекся этой функцией.

Хотелось бы увидеть поддержку распределения кучи для CTFE после того, как это будет сделано. Я не знаю, заинтересованы ли вы или кто-либо еще в работе над этим, но если нет, возможно, я мог бы помочь.

@alexreg Спасибо!

Обсуждение распределения кучи во время компиляции окончено на rust-rfcs / const-eval # 20. AFAIK, самые последние разработки касались парадигмы ConstSafe / ConstRefSafe для определения того, что можно наблюдать непосредственно / за ссылкой в ​​окончательном значении const . Я думаю, что нужно еще поработать над дизайном.

Для тех, кто следит за этим, # 65949 (который сам зависит от нескольких небольших PR) является следующим препятствием для этого. Хотя это может показаться лишь косвенно связанным, тот факт, что проверка констант / квалификация были так тесно связаны с продвижением, был одной из причин, по которой эта функция была заблокирована так долго. Я планирую открыть следующий PR, который полностью удалит старую const-checker (в настоящее время мы запускаем обе проверки параллельно). Это позволит избежать неэффективности, о которой я упоминал ранее.

После того, как оба вышеупомянутых PR объединены, if и match в константах будут немного улучшены в диагностике и отключены от функции! Да, еще тесты, столько тестов ...

Если вам нужны тесты, я не знаю, с чего начать, но я более чем готов внести свой вклад! Просто дайте мне знать, где должны проходить тесты / как они должны выглядеть / на какой ветке я должен базировать код :)

Следующий PR, который стоит посмотреть - # 66385. Это полностью удаляет старую логику квалификации const (которая не могла обрабатывать ветвление) в пользу новой версии на основе потока данных.

@ jyn514 Было бы здорово! Я пингуюсь, когда начну работать над реализацией. Также было бы очень полезно попытаться нарушить безопасность const (особенно часть HasMutInterior ), когда if и match доступны по ночам.

66507 содержит начальную реализацию RFC 2342.

Я полагаю, что потребуется некоторое время, чтобы устранить острые углы, особенно в отношении диагностики, а покрытие тестами довольно скудное ( @ jyn514, мы должны согласовать этот вопрос). Тем не менее, я надеюсь, что мы сможем выпустить это с пометкой функции в ближайшие несколько недель.

Это было реализовано в # 66507 и теперь может использоваться в последней версии nightly. Также есть сообщение в блоге Inside Rust, в котором подробно описаны новые доступные операции, а также некоторые проблемы, с которыми вы можете столкнуться в существующей реализации типов с внутренней изменчивостью или пользовательским Drop impl.

Идите и подтвердите!

Кажется, равенство не const ? Или я ошибаюсь:

error[E0019]: constant function contains unimplemented expression type
  --> src/liballoc/raw_vec.rs:55:22
   |
55 |         let cap = if mem::size_of::<T>() == 0 { !0 } else { 0 };
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^

error[E0019]: constant function contains unimplemented expression type
  --> src/liballoc/raw_vec.rs:55:19
   |
55 |         let cap = if mem::size_of::<T>() == 0 { !0 } else { 0 };
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to 2 previous errors

@ mark-im Это действительно должно

Не уверен, что это намеренно, но попытка сопоставления по перечислению дает ошибку

const fn с недостижимым кодом нестабильна

несмотря на то, что перечисление является исчерпывающим и определено в одном ящике.

@jhpratt можешь опубликовать код? Я могу без проблем сопоставить простые перечисления: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=585e9c2823afcb49c6682f69569c97ea

@jhpratt можешь опубликовать код? Я могу без проблем сопоставить простые перечисления:

здесь:
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=13a9fbc4251d7db80f5d63b1dc35a98b

Обыграй меня на пару секунд. Это минимальный пример, демонстрирующий мой конкретный случай.

@jhpratt Определенно не намеренно. Не могли бы вы открыть вопрос?

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

@Centril Неплохо поместить в верхний комментарий, чтобы ваш не был похоронен.

Обновление статуса:

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

const fn foo<T>() {
    let x = Option::<T>::None;
    {x};
}

который будет отклонен анализом на основе типов, потому что Option<T> может иметь деструкторы, которые теперь попытаются запустить и, таким образом, могут выполнить неконстантный код.

Мы можем вернуться к анализу на основе типов в тот момент, когда есть ветки, но это будет означать, что мы отклоним

const fn foo<T>(b: bool) {
    let x = Option::<T>::None;
    assert!(b);
    {x};
}

что, вероятно, было бы очень удивительно для пользователей.

@ ecstatic-morse провела анализ всех функций, а не только const fn, и увидела замедление до 5% (https://perf.rust-lang.org/compare.html?start=93dc97a85381cc52eb872d27e50e4d518926a27c&end=51cf313bec61139ae365d365). Обратите внимание, что это пессимистическая версия, поскольку это означает, что она также выполняется на функциях, которые не будут и часто никогда не могут стать const fn .

Это означает, что если мы сделаем множество функций const fn, мы можем увидеть некоторые замедления компиляции из-за этого анализа на основе значений.

Компромиссным вариантом может быть выполнение анализа на основе значений только в случае сбоя анализа на основе типов. Это означает, что если нет деструкторов, нам не нужно запускать анализ на основе значений, чтобы выяснить, не будут ли запущены несуществующие деструкторы (да, я знаю, здесь множество отрицаний). Другими словами: мы запускаем анализ на основе значений только при наличии деструкторов.

Я предлагаю это для обсуждения @ rust-lang / lang, чтобы мы могли понять, хотим ли мы использовать

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

@ oli-obk

вариант на основе типа при наличии циклов или ветвей (что приводит к странному поведению пользователей)

Просто чтобы проверить это: разве это не вариант иметь анализ на основе типов даже в прямом коде? Я полагаю, что это своего рода обратная несовместимость, учитывая, что мы уже принимаем следующее ( игровая площадка ):

struct Foo { }

impl Drop for Foo {
    fn drop(&mut self) { }
}

const T: Option<Foo> = None;

fn main() { }

Лично я склонен думать, что мы должны добиваться более последовательного и лучшего опыта для пользователей. Похоже, что мы можем оптимизировать по мере необходимости, и в любом случае стоимость не так уж и велика. Но я хотел бы немного лучше понять, что именно происходит в этом более дорогостоящем анализе: идея о том, что мы в основном выполняем «постоянное распространение», так что всякий раз, когда что-то сбрасывается, мы анализируем точное значение, которое сбрасывается, чтобы определить, действительно ли он может содержать значение, которое необходимо для запуска деструктора? (то есть, если это None , чтобы использовать общий пример Option<T> )

Просто чтобы проверить это: разве это не вариант иметь анализ на основе типов даже в прямом коде? Я полагаю, что это своего рода обратная несовместимость, учитывая, что мы уже принимаем следующее (игровая площадка):

Да, именно поэтому мы не можем полностью перейти к анализу на основе типов.

идея в том, что мы в основном выполняем «постоянное распространение», так что всякий раз, когда что-то отбрасывается, мы анализируем точное отбрасываемое значение, чтобы определить, может ли оно содержать значение, которое необходимо для запуска деструктора? (то есть, если это None, чтобы использовать общий пример Option)

Мы только распространяем список флагов ( Drop и Freeze , я просто показал здесь Drop потому что это легче объяснить). Когда мы достигаем терминатора Drop не установив флаг Drop , мы игнорируем терминатор Drop . Это позволяет использовать следующий код:

{
    let mut x = None;
    // Drop flag for x: false
    let y = Some(Foo);
    // Drop flag for y: true
    x = y; // Dropping x is fine, because Drop flag for x is false
    // Drop flag for y: false, Drop flag for x: true
    x
    // Dropping y is fine, because Drop flag for y is false
}

Этого не происходит во время оценки, поэтому следующее недопустимо:

{
    let mut x = Some(Foo);
    if false {
        x = None;
    }
    x
}

Мы проверяем, что все возможные пути выполнения не вызывают Drop .

Тем не менее, постоянное распространение - хорошая аналогия. Это еще одна проблема с потоком данных, передаточная функция которой не может быть выражена с помощью наборов gen / kill, которые не обрабатывают копирование состояния между переменными. Однако при распространении констант необходимо сохранять фактическое значение каждой переменной, а при проверке констант необходимо хранить только один бит, указывающий, имеет ли эта переменная пользовательский Drop impl или не является Freeze что делает его немного дешевле, чем постоянное распространение.

Чтобы было ясно, первый пример @ oli-obk компилируется на стабильной версии сегодня, а начиная с

Кроме того, const X: Option<Foo> = None; компилируется, начиная с версии 1.0, все остальное является естественным расширением этого с новыми функциями, которые получил const eval.

Хорошо, я считаю, что тогда имеет смысл принять чисто ценностный вариант.

Думаю, мы сможем осветить это на встрече и доложить =)

Резюме

Я предлагаю стабилизировать #![feature(const_if_match)] текущей семантикой.

В частности, выражения if и match а также логические операторы короткого замыкания && и || станут допустимыми во всех константных контекстах . Контекст констант может быть любым из следующих:

  • Инициализатор дискриминанта const , static , static mut или перечисления.
  • Тело const fn .
  • Значение константы generic (только в ночное время).
  • Длина типа массива ( [u8; 3] ) или выражения повторения массива ( [0u8; 3] ).

Кроме того, логические операторы короткого замыкания больше не будут понижены до их побитовых эквивалентов ( & и | соответственно) в инициализаторах const и static (см. № 57175). В результате привязки let могут использоваться вместе с логикой короткого замыкания в этих инициализаторах.

Проблема с отслеживанием: # 49146.
Целевая версия: 1.45 (16.06.2020)

История внедрения

64470 реализовал статический анализ на основе значений, который поддерживал условный поток управления и был основан на потоке данных. Это, вместе с # 63812, позволило нам заменить старый код проверки констант на тот, который работал со сложными графами потока управления. Старый const-checker какое-то время запускался параллельно с основанным на потоке данных, чтобы убедиться, что они согласовали программы с простым потоком управления. # 66385 удалил старую проверку констант в пользу проверки на основе потока данных.

66507 реализовал вентиль функции #![feature(const_if_match)] с семантикой, которая сейчас предлагается для стабилизации.

Квалификация Const

Задний план

[Miri] уже несколько лет поддерживает вычисление функций времени компиляции (CTFE) в rustc и может оценивать условные операторы по крайней мере так долго. Во время CTFE мы должны избегать определенных операций, таких как вызов пользовательского Drop impls или взятие ссылки на значение с внутренней изменчивостью . В совокупности эти дисквалифицирующие свойства известны как «квалификации», а процесс определения того, имеет ли значение квалификацию в конкретном пункте программы, известен как «константная квалификация».

Miri вполне может выдать ошибку, когда обнаруживает недопустимую операцию с квалифицированным значением, и может сделать это без ложных срабатываний. Однако CTFE происходит после мономорфизации, то есть он не может знать, допустимы ли константы, определенные в общем контексте, до тех пор, пока они не будут созданы, что может произойти в другом ящике. Чтобы получить ошибки предварительной мономорфизации, мы должны реализовать статический анализ, который выполняет квалификацию const. В общем случае квалификация const неразрешима (см . Теорему Райса ), поэтому любой статический анализ может только аппроксимировать проверки, которые Мири выполняет во время CTFE.

Наш статический анализ должен запретить упоминание типа с внутренней изменчивостью (например, &Cell<i32> ) в окончательном значении const . Если бы это было разрешено, const можно было бы изменить во время выполнения.

const X: &std::cell::Cell<i32> = std::cell::Cell::new(0);

fn main() {
  X.get(); // 0
  X.set(42);
  X.get(); // 42
}

Однако мы позволяем пользователю определять const , тип которого имеет внутреннюю изменчивость ( !Freeze ), если мы можем доказать, что окончательное значение этого const не имеет. Например, со времени первого выпуска стабильной ржавчины было скомпилировано следующее:

const _X: Option<&'static std::cell::Cell<i32>> = None;

Этот подход к статическому анализу, который я буду называть основанным на значениях, а не на основе типов, также используется для проверки кода, который может привести к вызову пользовательского Drop impl. Вызов Drop impls проблематичен, потому что они не проверяются константой и, следовательно, могут содержать код, который не был бы разрешен в константном контексте. Рассуждения на основе значений были расширены для поддержки операторов let , что означает следующие компиляции на стабильной версии rust 1.42.0 .

const _: Option<Vec<i32>> = {
  let x = None;
  let mut y = x;
  y = Some(Vec::new()); // Causes the old value in `y` to be dropped.
  y
};

Текущая ночная семантика

Текущее поведение #![feature(const_if_match)] расширяет семантику на основе значений для работы со сложными графами потока управления с помощью потока данных. Другими словами, мы пытаемся доказать, что переменная не имеет указанной квалификации на всех возможных путях прохождения программы.

enum Int {
    Zero,
    One,
    Many(String), // Dropping this variant is not allowed in a `const fn`...
}

// ...but the following code is legal under this proposal...
const fn good(x: i32) {
    let i = match x {
        0 => Int::Zero,
        1 => Int::One,
        _ => return,
    };

    // ...because `i` is never `Int::Many` on any possible path through the program.
    std::mem::drop(i);
}

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

const fn bad(b: bool) {
    let i = if b == true {
        Int::One
    } else if b == false {
        Int::Zero
    } else {
        // This branch is dead code. It can never be reached in practice.
        // However, const qualification treats it as a possible path because it
        // exists in the source.
        Int::Many(String::new())
    };

    // ILLEGAL: `i` was assigned the `Int::Many` variant on at least one code path.
    std::mem::drop(i);
}

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

#![feature(const_mut_refs)]

const fn none() -> Option<Cell<i32>> {
    None
}

// ILLEGAL: We must assume that `none` may return any value of type `Option<Cell<i32>>`.
const BAD: &Option<Cell<i32>> = none();

const fn also_bad() {
    let x = Option::<Box<i32>>::None;

    let _ = &mut x;

    // ILLEGAL: because a mutable reference to `x` was created, we can no
    // longer assume anything about its value.
    std::mem::drop(x)
}

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

Альтернативы

Мне было трудно придумать практические, обратно совместимые альтернативы существующему подходу. Мы могли бы вернуться к анализу на основе типов для всех переменных, как только условные выражения будут использоваться в константном контексте. Однако это также было бы трудно объяснить пользователям, поскольку кажущиеся несвязанными добавлениями код больше не компилируется, например assert в следующем примере из @ oli-obk.

const fn foo<T>(b: bool) {
    let x = Option::<T>::None;
    assert!(b);
    {x};
}

Повышенная выразительность ценностного анализа не бесплатна. Перфоманс, который выполнял квалификацию const для всех тел элементов, а не только для const , показал регрессию до const . Возможные оптимизации, такие как # 71330, обсуждались ранее в этой ветке.

Будущая работа

На данный момент проверка констант выполняется перед обработкой отбрасывания, что означает, что некоторые терминаторы отбрасывания остаются в MIR, которые на практике недоступны. Это предотвращает превращение Option::unwrap const fn (см. # 66753). Это несложно решить, но для этого потребуется разделить проход проверки констант на две фазы (проработка до и после отбрасывания).

После стабилизации #![feature(const_if_match)] можно сделать множество библиотечных функций const fn . Это включает в себя множество методов для примитивных целочисленных типов, которые перечислены в # 53718.

Циклы в контексте const блокируются в том же квалификационном вопросе const, что и условные выражения. Текущий подход на основе потока данных также работает для циклических CFG без каких-либо модификаций, поэтому, если #![feature(const_if_match)] стабилизируется, основной блокировщик для # 52000 исчезнет.

Благодарности

Особая благодарность выражается @ oli-obk и @eddyb , которые были основными рецензентами большей части работы по реализации, а также остальным участникам @ rust-lang / wg-const-eval за то, что они помогли мне понять соответствующие проблемы, связанные с const квалификация. Ничего из этого было бы невозможно без Мири, созданной @solson, а теперь поддерживаемой @RalfJung и @ oli-obk.

Предполагается, что это отчет о стабилизации, предшествующий FCP. Однако я не могу открыть FCP.

@ ecstatic-morse Большое спасибо за вашу тяжелую работу над этой проблемой!

Отличный отчет!

Одна вещь, которую я бы хотел увидеть, @ ecstatic-morse, - это

  • ссылки на некоторые репрезентативные тесты в репо, чтобы мы могли наблюдать за поведением
  • есть ли смысл в отношении semver или чего-то еще - я думаю, ответ в основном отрицательный , верно? Другими словами, мы выбираем анализ, используемый для определения того, является ли тело const fn законным, но, учитывая const fn, наш выбор здесь не определяет такие вещи, как «что может сделать вызывающий const fn с результат ", да? Я пытаюсь выяснить, какой может быть пример того, о чем я говорю - я предполагаю, что вызывающий не узнает точно, какие варианты перечисления использовались, только это - какое бы значение был возвращен - у него не было внутренней изменчивости (с которой они, по-видимому, тоже не могут полагаться при сопоставлении).

Другими словами, мы выбираем анализ, используемый для определения того, является ли тело const fn законным, но, учитывая const fn, наш выбор здесь не определяет такие вещи, как «что может сделать вызывающий const fn с результат ", да? Я пытаюсь выяснить, какой может быть пример того, о чем я говорю - я предполагаю, что вызывающий не узнает точно, какие варианты перечисления использовались, только это - какое бы значение был возвращен - у него не было внутренней изменчивости (с которой они, по-видимому, тоже не могут полагаться при сопоставлении).

Да, тело const fn непрозрачно. Это контрастирует с выражением инициализатора const item. Вы можете заметить это по тому факту, что

const FOO: Option<Cell<i32>> = None;

можно использовать для создания &'static Option<Cell<i32>>

const BAR: &'static Option<Cell<i32>> = &FOO;

в то время как const fn с тем же телом не может:

const fn foo() -> Option<Cell<i32>> { None }
const BAR: &'static Option<Cell<i32>> = &foo();

демонстрация игровой площадки

Когда мы вводим поток управления в константы, это означает, что

const FOO: Option<Cell<i32>> = if MEH { None } else { None };

также будет работать независимо от значения MEH и

const FOO: Option<Cell<i32>> = if MEH { Some(Cell::new(42)) } else { None };

не будет работать, опять же, независимо от значения MEH .

Поток управления ничего не меняет в сайтах вызова const fn , только в том, какой код разрешен внутри этой const fn.

ссылки на некоторые репрезентативные тесты в репо, чтобы мы могли наблюдать за поведением.

Я добавил параграф в конце раздела «Текущая ночная семантика», который содержит ссылки на некоторые интересные тестовые примеры. Я чувствую, что нам нужно больше тестов (утверждение, которое верно независимо от обстоятельств), прежде чем это будет стабилизировано, но это можно будет решить, как только мы решим, желательна ли текущая семантика.

есть ли смысл в отношении semver или чего-то еще.

В дополнение к тому, что @ oli-obk сказал выше, я хочу указать, что изменение окончательного значения const технически уже является серьезным изменением:

// Upstream crate
const IDX: usize = 1; // Changing this to `3` will break downstream code!

// Downstream crate

extern crate upstream;

const X: i32 = [0, 1, 2][upstream::IDX]; // Only compiles if `upstream::IDX <= 2`

Однако, поскольку мы не можем выполнить квалификацию const с идеальной точностью, изменение константы на использование if или match может нарушить последующий код, даже если окончательное значение не изменится. Например:

// Changing from `cfg` attributes...

#[cfg(not(FALSE))]
const X: Option<Vec<i32>> = None;
#[cfg(FALSE)]
const X: Option<Vec<i32>> = Some(Vec::new());

// ...to the `cfg` macro...

const X: Option<Vec<i32>> = if !cfg!(FALSE) { None } else { Some(Vec::new() };

// ...could break downstream crates, even though `X` is still `None`!

// Downstream

 // Only legal if static analysis can prove the qualifications in `X`
const _: () =  std::mem::drop(upstream::X); 

Это не относится к изменениям внутри тела const fn , потому что мы всегда используем квалификацию на основе типа для возвращаемого значения, даже в пределах одного ящика.

На мой взгляд, «первородным грехом» здесь не было возвращение к квалификации на основе типа для const и static определенных во внешних ящиках. Однако я считаю, что так было с версии 1.0, и подозреваю, что от этого зависит довольно много кода. Как только вы разрешите константные инициализаторы, для которых статический анализ не может быть идеально точным, становится возможным изменить эти инициализаторы таким образом, чтобы они выдавали то же значение без возможности статического анализа доказать это.

редактировать:

В этом отношении нет ничего уникального в if и match . Например, в настоящее время критическим изменением является рефакторинг инициализатора const в const fn если последующий ящик полагался на квалификацию на основе значений.

// Upstream
const fn none<T>() -> Option<T> { None }

const VALUE_BASED: Option<Vec<i32>> = None;
const TYPE_BASED: Option<Vec<i32>> = none();

// Downstream

const OK: () = { std::mem::drop(upstream::VALUE_BASED); };
const ERROR: () = { std::mem::drop(upstream::TYPE_BASED); };

@ ecstatic-morse Спасибо, что написали отчет о стабилизации! Давайте оценим консенсус асинхронно:

@rfcbot объединить

Если кто-то хочет обсудить это синхронно на собрании, пожалуйста, повторно номинируйте его.

Член команды @joshtriplett предложил объединить это. Следующим шагом является проверка остальными членами команды, отмеченными тегами:

  • [x] @cramertj
  • [x] @joshtriplett
  • [x] @nikomatsakis
  • [x] @pnkfelix
  • [] @scottmcm
  • [] @withoutboats

В настоящее время не перечислено никаких проблем.

После того, как большинство рецензентов одобрит (и не более двух утверждений остаются невыполненными), наступит последний период для комментариев. Если вы заметили серьезную проблему, которая не поднималась ни на одном этапе этого процесса, сообщите об этом!

См. Этот документ для получения информации о том, какие команды могут дать мне члены команды, отмеченные тегами.

: bell: Сейчас наступает последний период для комментариев , как указано в обзоре выше . : звонок:

Позволяет ли это также использовать ? в const fn ?

Использование ? означает использование трейта Try . Использование трейтов в const fn нестабильно, см. Https://github.com/rust-lang/rust/issues/67794.

@TimDiekmann пока что вам придется писать макросы proc, которые понижают? вручную. То же самое касается loop и for , по крайней мере, до определенного предела (стиль примитивной рекурсии), но const eval в любом случае имеет такие ограничения. Эта функция настолько хороша, что позволяет делать МНОГО вещей, которые раньше были невозможны. Вы даже можете построить крошечный wasm vm в const fn, если хотите.

Последний период комментариев с предложением о слиянии в соответствии с приведенным выше обзором завершен .

Как автоматизированный представитель процесса управления, я хотел бы поблагодарить автора за их работу и всех, кто внес свой вклад.

RFC скоро будет объединен.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги