两个 rust bugs

好久没有记过流水帐了,记一点毫无意义的流水帐。

问题

大概三个星期之前我发现rustc两个小bug,其中有一个是非常有误导性的错误提示。我因此花了不少时间才找到我写的代码为什么不能编译的原因。 看了几天rust的代码后我提交了一个pull request。和我料想的一样,有一些回归测试失败了。此处rust的实现本来就太脆了,很容易出现

irrelevant-code.jpg

最近已经有几个pull requests被合并了。今天我想看下能不能改进一下这个pr,如果能过了测试,那我可能就能多一个commit了。 不得不说我搞不定这个问题(确切地说,我不能修好这个问题并且不引入新的问题)。我还要宣布这他妈的是我见过的最垃圾的代码,我王境泽就是饿死也不会再改这种垃圾代码了。

我发现的第一个问题是

fn main() {
    let xs: Vec<u64> = vec![1, 2, 3];
    // assert_eq!(6u64, xs.iter().sum()); // This works
    assert_eq!(xs.iter().sum(), 6u64);
}

这个问题看起来并不好修,因为没有明确的相关信息,我只能知道 rust 的类型推断出问题了。得花不少工夫才能找到相关代码。我之前也没有看过 rust 代码,所以直接放弃。

第二个问题是

fn main() {
    let xs: Vec<u64> = vec![1, 2, 3];
    println!("{}", 23u64 + xs.iter().sum());
}

其错误提示是

69455.png

没法编译的真实原因是rust无法推断出xs.iter().sum()的类型。在我当时出错的代码里,xs23u64 + xs.iter().sum()大约只有了10万行代码的距离。 所以我花了不少工夫才找出这个minimal reproducible case。

这个错误提示的原因是,rust推断出了main函数body中有类型推断错误,于是调用need_type_info_err报出错误信息。 need_type_info_err这个函数只接收到了出现错误的类型、出现错误的body和产生ambiguity的span(这可能并不是产生错误的真实位置)。 编译器需要根据类型信息从body中找出具体的出错位置、和可能出错的原因。

在我的情形中,出错的类型是u64Vec是一个kind为* -> *的、需要一个参数的泛型。在上面例子里,xs会接收到u64后,其类型是Vec<u64>。 在有些情况下,用户没有明确指定泛型参数会给编译器带来错误,所以编译器会提示用户需要明确指定泛型参数。

很遗憾,在我的情形下,这是个red herring。真正有用的信息在于note: cannot resolve <u64 as std::ops::Add<_>>::Output == _。把代码改成

fn main() {
    let xs: Vec<u64> = vec![1, 2, 3];
    let temp = xs.iter().sum();
    println!("{}", 23u64 + temp);
}

就可以清楚地看出这个note是什么意思了。这个时候rust会提示我们给temp指定一个类型。 因为rust没法推断出xs.iter().sum()的类型,所以它才没法知道u64在这里到底是哪个trait std::ops::Add<_>+的右手边的操作数是可以有多种不同类型的。

这个问题的难修并不是难于找到出错的位置,而是难于改动出错的代码。

此处代码有几个地方很坑。

  • 第一是他没有直接传递ambiguity只传递了类型,need_type_info_err visit整个ast的时候找到的对应类型可能并不是直接产生ambiguity的地方。

  • 第二是visit ast的时候会对状态进行改变。有好几种错误都会改动found_ty这个变量。没有办法得知found_ty是在什么情况下改动的。

  • 第三是对不同类型的错误的处理都是一些partial函数。它们之前可能会有重叠。改动处理顺序就可能产生意想不到的后果。而真正处理的时候又认为它们完全互斥。

  • 第四是为了复用整个逻辑它中间又设置了几个所有错误类型都可以用上的变量ty_msgsuffix等,这些变量有可能在两个不同的类型中定义然后使用。

  • 第五是为了应对某些错误类型的特殊性,又增加了单独处理逻辑。

  • 第六是还有一些表达式没有考虑到,比如说+的实际表示是一个biops,这并不在考虑过的类型中。所以 visit ast 的时候是没有发现针对+的错误的。

  • 第七是有些情况下可能出现多种错误,这些错误可能会设置同一变量,比如found_ty。

基于上面原因,我有了几次失败的尝试。

  • 彻底重写,把根据不同的所有的错误拆分错误处理逻逻辑,不让其有交叉的地方。不想出现任何莫名设置的变量。 这个尝试失败了,因为第一有些情况下可能出现多种错误,第二我统一处理之后,发现没法明白有一部分的逻辑,第三有些地方逻辑上没法隔离开来(比如 found_ty 和 found_method)。

  • 局部微调,在我的情形里rust发现的不成功的断言叫ty::Predicate::Projection。调用need_type_info_err会把+的span传递过去。但是 visitor 没有使用这个信息, 我可以在代码里面加上如果method的receiver是错误类型并且method的span就是出错的span就判定此处有错。加上这个逻辑之后,又有好几个测试失败了。 原因是这种方法太精确地找到了出错的位置。比如在下面这个例子里

for i in [].iter() {
    i.clone()
}

原先的错误可以提示我 rust 没法得知 iterator 的元素类型,现在我只能知道我需要额外给 i 提供类型信息。之前的信息明显更加 informative。 再比如说对于 buffer.last().unwrap().0.clone(),我现在会提示 rust 没法得知 buffer 的类型,原先的提示是 rust 没法得知 buffer.last() 作为一个 Option 它的泛型参数。

所以我如果又需要在这个 need_type_info_err 上面加上针对这些特殊情形的错误处理逻辑。所以只能希望他们另请高明了。

这个代码真的太脏了。但是正是这种肮脏的代码让我觉得 rust 是一个好语言。正如https://matklad.github.io/2020/02/14/why-rust-is-loved.html 说得一样,rust 的可爱之处都是在这些 Small Things 上面。此处的肮脏代码显然不能考虑到所有的错误情形,但是针对大多数错误情形,我们可以得到非常人性化的错误提示。 从这个地方的代码堆的逻辑可以看出,rust 贡献者为了给我们一个有用的错误信息,真是殚精竭虑。

总结

再记一点怎么给rust提patch的流水帐。

首先是编译环境的搭建,作为一个nix用户,每编译一个未知的软件,我都要喷一次其它发行版就是垃圾。 nix 配合 nixpkgs 可以让我不需要了解一个软件的依赖快速从源代码编译这个软件。

通常我会起一个新的 nix-shell nix-shell -E 'with <nixpkgs> { }; xxx'。 然后运行 out=out src=. genericBuild 来快速地构建一个软件。可惜的是这对 rustc 不起效果。 因为 rustc dev 没法用 rustc stable stage0 的 binary 来 bootstrap。在我的情形里是最新的rust源代码用到了libstd里面的 Result::map_or 这个函数。

我的另一个失败尝试是使用mozilla 的overlay,和最新的 nightly 来bootstrap

nix-shell -E 'with (import <nixpkgs> { overlays = [ (import (builtins.fetchTarball https://github.com/mozilla/nixpkgs-mozilla/archive/master.tar.gz))  ];  }); latest.rustChannels.nightly.rustc'

结果还是不行。我他妈的 checkout 到 nightly 这个 commit 还是不行。这种情况确实是存在的,1.42就不能编译出它自己。 我当时不知道通常是beta能编译出当前的的开发版本。在我就要放弃了的时候,我发现了这个 godsend。 然后 cached-nix-shell veritas/shells/rust.nix,volia。

普通rust程序的编译已经很慢了,rustc编译更是受不了地慢。在我的电脑上从零开始编译(包括llvm)得两个多小时。增量编译一开始只要6分钟,后来不知道我干啥了,变成了30分钟。 关键是,这个时候我基本是干不了活的,实在太卡了。控制一下线程数又太慢了。所以使用一个debugger就很有必要了。

我和rust-gdb之中至少有一个垃圾。要不就是我太垃圾不能理解它,要不就是它太垃圾我没法理解它。这下面的事情一个也干不了。

  • pretty-print,做不到 pretty-print.png

  • 我要取出和类型数据的分支,干不了

  • 我要取出Vec里的某个元素,干不了

  • 我要把指针地址转成任意数据,干不了

  • 我要把指针地址转成字符串,不好干

  • 我要调用 trait 的方法,干不了

  • 我要打印{:?},干不了

  • 还有如果我要打印一个 debug 信息,理论上我可以找一下 fmt::debug::Debug 的实现,实际上,比如说Span这个struct打印出来是这样的。这个根本看不出来任何东西。

span.png

Spanfmt::debug::Debug的实现实际上非常复杂,它有一个默认实现是打印出上面的记录,有一个 atomic reference 指向这个实现, rustc_driver 在启动之后会用rustc_span::SPAN_DEBUG.swap(&(span_debug as fn(*, &mut fmt::Formatter<'_>) -> _))取代这个函数, 我要调用真正的函数不是那么容易。

rr 好像也用不了,

rr record ~/.rustup/toolchains/debug/bin/rustc ~/Workspace/playground/rust/src/bin/69455.rs
rr: Saving execution to trace directory `/home/e/.local/share/rr/rustc-10'.
/home/e/.rustup/toolchains/debug/bin/rustc: relocation error: /nix/store/1ncwrl8bplq3xhmj8pxfkx4y0i90vmnx-glibc-2.30/lib/libpthread.so.0: symbol \_\_write_nocancel version GLIBC_PRIVATE not defined in file libc.so.6 with link time reference

最他妈恼火的是,编译器优化关不了。 所以打印一个参数,你得

optimized.png

加油吧,同志们,关于 rust-gdb 我们大约可以提交一亿个 commits。

Publié le par v dans «misc». Mots-clés: rust