Rust: RFC 2342 的跟踪问题,“在常量中允许 `if` 和 `match`”

创建于 2018-03-18  ·  83评论  ·  资料来源: rust-lang/rust

这是 RFC "Allow ifmatch in constants" (rust-lang/rfcs#2342) 的跟踪问题。

请将您想要报告的特定功能或问题的结构重定向到新问题,并用F-const_if_match适当地标记它们,这样这些问题就不会被短暂的评论淹没,从而掩盖了重要的发展。

脚步:

  • [x] 实施 RFC
  • [] 调整文档(参见伪造说明
  • [x] 稳定 PR(参见伪造说明
  • [x] 使用&&||短路操作的常量中的let绑定。 这些现在被视为&|内的conststatic项目。

未解决的问题:

没有任何

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 限定对查询系统进行一些更改,以确保启用此功能时不会不必要地降低效率。 我们正在这里取得进展,我相信这个实验性的实现将在数周而不是数月的每晚提供(著名的遗言:微笑:)。

所有83条评论

  1. 为它添加一个特征门
  2. https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L347 中的switchswitchInt终结符需要自定义代码如果特征门处于活动状态
  3. 而不是有一个当前的基本块(https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L328)这需要是一些具有列表的容器它仍然需要处理的基本块。

@oli-obk 这有点棘手,因为复杂的控制流意味着需要使用数据流分析。 我需要回到@alexreg并弄清楚如何整合他们的更改。

@eddyb一个好的起点可能是采用我的const-qualif分支(减去最高提交),将其重新设置为 master(不会很有趣),然后添加数据注释内容,对吧?

有这方面的消息吗?

@mark-im 唉,不。 我认为@eddyb确实很忙,因为过去几周我什至无法在 IRC 上 ping 他哈哈。 遗憾的是我的 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抱歉,我已经切换到本地睡眠时间表,我看到你在我醒来时给我发了 ping,但是当我醒着时你整天都处于离线状态(呃时区)。
我应该从你的分支机构做一个 PR 吗? 我忘了我们应该用它做什么?

@eddyb 没关系,呵呵。 你必须早点睡觉,因为我通常从格林威治标准时间晚上 8: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应该没问题,但有些人希望const fn在运行时是“纯的”(即“引用透明”或类似的),这意味着const fn作为参数获得的引用后面读取是可以的,但是const fn永远不能凭空获得static的引用(包括从const )。

我认为我们可以继续静态地拒绝在const s、 const fn和其他常量上下文(包括促销)中提及static s(即使只是为了参考它们)。
但是我们仍然必须删除STATIC_REF hack,它允许static s 获取其他static s 的引用,但是(尝试失败并失败)拒绝从这些引用后面读取.

我们需要为此制定 RFC 吗?

从静力学读起来听起来很公平。 怀疑它需要一个 RFC,也许只是一个火山口运行,但我可能不是最好的说法。

请注意,我们不会限制任何东西,我们会放宽已经打破的限制。

哦,我看错了。 所以 const 评估仍然是合理的,只是引用不透明?

最后一段描述了一种引用透明的方法(但如果我们开始允许在const s 和const fn s 中提及static s,我们就会失去这个属性)。 我不认为是否真的在讨论健全性。

好吧,“悬空指针”确实听起来像是一个健全的问题,但我会相信你!

“悬空指针”是一个糟糕的错误消息,这只是 miri 禁止从static s 读取。 唯一可以引用static的常量上下文是其他static ,因此我们可以“只”允许这些读取,因为所有这些代码总是在编译时运行一次。

(来自 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的想法是const fn进行某些原始指针操作(例如比较和转换为整数) unsafe const fn (我们可以这样做,直到我们稳定const fn ),并声明它们只能以 miri 在编译时允许的方式使用。
例如,将两个指针减去到同一个本地应该没问题(您得到的相对距离仅取决于类型布局、数组索引等),但格式化引用的地址(通过{:p} )是不正确的使用,因此fmt::Pointer::fmt不能标记为const fn
同样,原始指针的Ord / Eq trait impls 都不能被标记为const (只要我们能够这样注释它们),因为它们是安全的但操作是unsafe in const fn

取决于你所说的“无害”是什么意思......我当然可以理解我们想要禁止这种非确定性行为的原因。

如果继续进行这方面的工作,那就太好了。

@lachlansneff它正在移动......没有我们想要的那么快,但工作正在完成。 目前我们正在等待https://github.com/rust-lang/rust/pull/51110作为拦截器。

@alexreg啊,谢谢。 即使不在 const fn 中,也能够将匹配或 if 标记为 const 将非常有用。

#51110 合并后是否有任何状态更新?

@programmerjake在合并之前,我正在等待@eddybhttps://github.com/rust-lang/rust/pull/52518上的一些反馈(希望很快)。 他最近很忙(总是需求量很大),但过去几天他又回到了评论和诸如此类的东西,所以我充满希望。 在那之后,我怀疑这需要他个人的一些工作,因为添加适当的数据流分析是一件复杂的事情。 我们会看到的。

在第一篇文章的待办事项列表的某个地方,应该添加它以删除当前将&&||&|可怕黑客

@RalfJung 那不是旧的 const eval 的一部分吗,现在 MIRI CTFE 已经到位,它已经完全消失了吗?

AFAIK 我们在 HIR 降低的某个地方进行了翻译,因为我们在const_qualify中有代码拒绝SwitchInt终止符,否则|| / &&会生成终止符。

另外,另一点:@oli-obk 在某处(但我找不到在哪里)说,条件在某种程度上比人们天真地想象的要复杂......这“只是”关于 drop/interior 可变性的分析吗?

这“只是”关于跌落/内部可变性的分析吗?

我目前正在努力解决这个问题。 当我得到所有信息时会回复你

这是什么状态? 这是需要人力还是解决一些问题时受阻?

@mark-im 在为常量限定实施适当的数据流分析时被阻止。 @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已经被make as usize casts 覆盖(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 有打算,也许这个月或下个月(从我上次与他们谈过这件事开始),尽管我可以't/不会代表他们做出承诺。

@alexreg @mark-im @est31 @oli -obk 我应该能够在本周某个时候发布基于数据流的 const 限定的 WIP 实现。 这里存在很多兼容性隐患,因此实际合并可能需要一段时间。

极好的; 对此期待。

(每个请求从 #57563 复制)

是否可以对bool && boolbool || bool等进行特殊处理? 它们目前可以在const fn ,但这样做需要按位运算符,这有时是不需要的。

它们已经在conststatic项目中进行了特殊处理——通过将它们转换为按位运算。 但是这种特殊的外壳是一个巨大的黑客,很难确保这实际上是正确的。 正如你所说,它有时也是不需要的。 所以我们宁愿不经常这样做。

做正确的事情需要一点时间,但它会发生。 如果我们同时积累了太多的 hack,我们可能会将自己困在一个无法摆脱的角落(如果其中一些 hack 最终以错误的方式交互,从而意外地稳定了我们不想要的行为)。

现在#64470 和#63812 已经合并,编译器中存在所有需要的工具。 我仍然需要围绕 const 限定对查询系统进行一些更改,以确保启用此功能时不会不必要地降低效率。 我们正在这里取得进展,我相信这个实验性的实现将在数周而不是数月的每晚提供(著名的遗言:微笑:)。

@ecstatic-morse 很高兴听到! 感谢你们共同努力完成这项工作; 我个人一直热衷于这个功能一段时间了。

完成此操作后,希望看到对 CTFE 的堆分配支持。 我不知道您或其他任何人是否有兴趣从事此工作,但如果没有,也许我可以提供帮助。

@alexreg谢谢!

关于编译时堆分配的讨论在 rust-rfcs/const-eval#20 结束。 AFAIK,最近的发展是围绕ConstSafe / ConstRefSafe范式来确定可以在const的最终值中直接/在引用后面观察到的内容。 我认为还需要更多的设计工作。

对于后面的人,#65949(它本身取决于一些较小的 PR)是下一个阻止程序。 虽然它可能看起来只是切线相关,但常量检查/限定与升级如此紧密结合的事实是此功能被阻止这么长时间的部分原因。 我计划打开一个后续的 PR,它将完全删除旧的 const-checker(目前我们并行运行两个检查器)。 这将避免我之前提到的低效率。

在上述两个 PR 合并后,常量中的ifmatch将是一些诊断改进和功能标志! 哦,还有测试,这么多测试......

如果您需要测试,我不确定如何开始,但我非常愿意做出贡献! 让我知道测试应该去哪里/它们应该是什么样子/我应该从哪个分支开始编写代码:)

下一个要观看的 PR 是 #66385。 这完全删除了旧的 const 限定逻辑(无法处理分支),以支持新的基于数据流的版本。

@jyn514那太好了! 当我开始起草实施时,我会联系你。 一旦ifmatch每晚可用,人们试图违反const 安全(尤其是HasMutInterior部分)也会非常有帮助。

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 = debug&edition =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=51cf463030373303730cf39f3ae) 请注意,这是一个悲观的版本,因为这意味着它也运行在不会并且通常永远不会成为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的常见例子)

我们只传播一个标志列表( DropFreeze ,我只是在这里展示了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 的第一个示例今天在稳定版上编译,并且从1.38.0 开始,其中不包括 #64470。

此外, const X: Option<Foo> = None;从 1.0 开始编译的,其他所有内容都只是自然扩展,具有 const eval 获得的新功能。

好的,我相信那时采用纯粹基于价值的选项是有意义的。

我想我们可以在会议上讨论它并报告 =)

概括

我建议我们用当前的语义来稳定#![feature(const_if_match)]

具体来说, ifmatch表达式以及短路逻辑运算符&&||将在所有const 上下文中变得合法。 const 上下文是以下任何一种:

  • conststaticstatic mut或枚举判别式的初始值设定项。
  • const fn的主体。
  • const 泛型的值(仅限每晚)。
  • 数组类型 ( [u8; 3] ) 或数组重复表达式 ( [0u8; 3] ) 的长度。

此外,在conststatic初始值设定项中,短路逻辑运算符将不再降低为它们的按位等价物(分别&| )(参见#57175)。 因此, let绑定可以与这些初始值设定项中的短路逻辑一起使用。

跟踪问题:#49146
版本目标:1.45 (2020-06-16)

实施历史

64470 实现了基于值的静态分析,支持条件控制流并基于数据流。 这与#63812 一起,允许我们用处理复杂控制流图的代码替换旧的常量检查代码。 旧的 const-checker 与基于数据流的检查器并行运行了一段时间,以确保它们就具有简单控制流的程序达成一致。 #66385 删除了旧的 const-checker 以支持基于数据流的 const-checker。

66507 实现了#![feature(const_if_match)]功能门,其语义现在被提议用于稳定性。

常备资格

背景

[Miri] 多年来一直在rustc为编译时函数求值 (CTFE) 提供支持,并且已经能够对条件语句求值至少那么长时间。 在 CTFE 期间,我们必须避免某些操作,例如调用自定义Drop impls或引用具有内部可变性的值。 这些不合格的属性统称为“限定”,在程序中的特定点确定值是否具有限定的过程称为“常量限定”。

当 Miri 遇到对限定值的非法操作时,它完全有能力发出错误,并且不会出现误报。 但是,CTFE 发生在单态化之后,这意味着它无法知道在通用上下文中定义的常量是否有效,直到它们被实例化,这可能发生在另一个 crate 中。 为了得到单态化前的错误,我们必须实现一个静态分析,做 const 限定。 在一般情况下,const 限定是不可判定的(参见莱斯定理),因此任何静态分析都只能近似于 Miri 在 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的最终没有。 例如,从stable rust 第一版开始编译如下:

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

这种静态分析方法(我将其称为基于值而不是基于类型)还用于检查可能导致调用自定义Drop impl 的代码。 调用Drop impls 是有问题的,因为它们没有经过常量检查,因此可能包含在常量上下文中不允许的代码。 基于值的推理被扩展为支持let语句,这意味着以下编译在 rust 1.42.0 stable 上

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);
}

此分析将函数调用视为不透明,假设它们的返回值可能包含其类型的任何值。 一旦创建了对变量的可变引用,我们也会回退到基于类型的变量分析。 请注意,目前在稳定 Rust 上禁止在 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)
}

您可以看到更多关于基于值的分析如何在内部可变性自定义 drop impls 方面保守的不会发生任何非法事件的示例

备择方案

我发现很难为现有方法提出实用的、向后兼容的替代方案。 一旦在 const 上下文中使用条件,我们就可以回退到对所有变量的基于类型的分析。 但是,这也很难向用户解释,因为看似无关的添加会导致代码不再编译,例如以下来自@oli-obk 的示例中的assert

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 的相关问题资格。 如果没有 Miri,这一切都不可能实现,Miri 由@solson创建,现在由@RalfJung和 @oli-obk 维护。

这旨在成为 FCP 之前的稳定报告。 但是,我无法打开 FCP。

@ecstatic-morse 非常感谢您在这个问题上的辛勤工作!

很棒的报告!

我想我想看到的一件事,@ecstatic-morse,是

  • 链接到 repo 中的一些代表性测试,因此我们可以观察行为
  • 是否对 semver 或其他任何东西有影响——我认为答案基本上是否定的,对吧? 换句话说,我们正在决定用于确定 const fn 的主体是否合法的分析,但是给定一个 const fn,我们在这里的选择并不能确定诸如“const fn 的调用者可以做什么”之类的事情结果”,对吗? 我试图弄清楚我正在谈论的一个例子可能是什么——我想可能是调用者无法准确地知道使用了枚举的哪些变体,只有那个——无论值被返回——它没有内部可变性(他们可能在匹配时也不能依赖,因为)。

换句话说,我们正在决定用于确定 const fn 的主体是否合法的分析,但是给定一个 const fn,我们在这里的选择并不能确定诸如“const fn 的调用者可以做什么”之类的事情结果”,对吗? 我试图弄清楚我正在谈论的一个例子可能是什么——我想可能是调用者无法准确地知道使用了枚举的哪些变体,只有那个——无论值被返回——它没有内部可变性(他们可能在匹配时也不能依赖,因为)。

是的,const fn 的主体是不透明的。 这与const项目的初始化表达式形成对比。 您可以通过以下事实观察到这一点

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 中允许的代码。

链接到 repo 中的一些代表性测试,因此我们可以观察行为。

我在“Current Nightly Semantics”部分的末尾添加了一个段落,链接到一些有趣的测试用例。 我觉得在稳定之前我们需要更多的测试(无论情况如何都是正确的陈述),但是一旦我们确定当前的语义是否可取,就可以解决这个问题。

是否对 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 限定,所以将常量更改为使用ifmatch可能会破坏下游代码,即使最终值没有改变。 例如:

// 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主体内部的更改,因为我们始终对返回值使用基于类型的限定,即使在同一个 crate 中也是如此。

在我看来,这里的“原罪”并没有退回到外部 crate 中定义的conststatic的基于类型的限定。 但是,我相信从 1.0 开始就是这种情况,我怀疑相当多的代码依赖于它。 一旦您允许静态分析不能完全精确的 const 初始值设定项,就可以修改这些初始值设定项,使它们产生相同的值而静态分析无法证明它。

编辑:

在这方面, ifmatch没有什么独特之处。 例如,这是目前一个重大更改,重构一个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

当前未列出任何问题。

一旦大多数审稿人批准(并且最多有 2 个未通过的批准),这将进入其最终评论期。 如果您发现在此过程中任何时候都没有提出的重大问题,请说出来!

有关标记的团队成员可以给我的命令的信息,请参阅此文档

:bell: 根据上面的评论

这是否也允许在const fn使用? const fn

使用?意味着使用Try特性。在const fn使用特性是不稳定的,参见https://github.com/rust-lang/rust/issues/67794。

@TimDiekmann目前,您必须编写 proc 宏来降低 ? 手动。 loopfor ,至少达到一定的限制(原始递归风格),但 const eval 无论如何都有这样的限制。 这个功能太棒了,它可以实现很多以前无法实现的功能。 如果需要,您甚至可以在 const fn 中构建一个很小的 ​​wasm vm。

最后的征求意见期,有倾向合并,具体根据上述审查,现已完成

作为治理过程的自动化代表,我要感谢作者的工作以及所有做出贡献的人。

RFC 将很快合并。

此页面是否有帮助?
0 / 5 - 0 等级