Somewhat naive working of a lint

#![deny(while_true)]
will correctly deny

while true
{
}

but will walk like a blind man into a mine field into:

while 1 < 2
{

}

That would require evaluating constants, which due to the halting problem is not possible in the general case. Sure, it could be made a bit less naive, but at what point does the amount of effort put into making it less naive not outweigh the benefits?

I strongly disagree with that statement. The compiler has that info already.

In this particular case it is possible. It is not possible in the general case.

Compiler already does evaluate constant expression at compile time. Every constant expression.

The general case is something like:

while condition() {
    // some code
}

Which is an impossible problem, even if condition is implemented entirely in Rust all the way down.

And I'm telling you that if condition is a constant expression, then it is very possible. And the compiler has all the info necessary to do that.

1 Like

This is not the case. Yes, it may evaluate it in some cases as optimization, but this is not guaranteed. If I disable MIR optimizations, 1 < 2 is not optimized to true.

$ echo "fn foo() {}                                                                                                             bjorn@laptopbjorn-lenovo

fn main() {
    while 1 < 2 {
        foo();
    }
}" | rustc +nightly - -Zmir-opt-level=0 --emit mir
$ cat rust_out.mir
// WARNING: This output format is intended for human consumers only
// and is subject to change without notice. Knock yourself out.
fn main() -> () {
    let mut _0: ();                      // return place in scope 0 at <anon>:3:11: 3:11
    let mut _1: ();                      // in scope 0 at <anon>:3:1: 7:2
    let mut _2: bool;                    // in scope 0 at <anon>:4:11: 4:16
    let _3: ();                          // in scope 0 at <anon>:5:9: 5:14
    let mut _4: !;                       // in scope 0 at <anon>:4:5: 6:6

    bb0: {
        StorageLive(_2);                 // scope 0 at <anon>:4:11: 4:16
        _2 = Lt(const 1_i32, const 2_i32); // scope 0 at <anon>:4:11: 4:16
        switchInt(_2) -> [false: bb1, otherwise: bb2]; // scope 0 at <anon>:4:5: 6:6
    }

    bb1: {
        _0 = const ();                   // scope 0 at <anon>:4:5: 6:6
        StorageDead(_2);                 // scope 0 at <anon>:6:5: 6:6
        return;                          // scope 0 at <anon>:7:2: 7:2
    }

    bb2: {
        StorageLive(_3);                 // scope 0 at <anon>:5:9: 5:14
        _3 = foo() -> bb3;               // scope 0 at <anon>:5:9: 5:14
                                         // mir::Constant
                                         // + span: <anon>:5:9: 5:12
                                         // + literal: Const { ty: fn() {foo}, val: Value(Scalar(<ZST>)) }
    }

    bb3: {
        StorageDead(_3);                 // scope 0 at <anon>:5:14: 5:15
        _1 = const ();                   // scope 0 at <anon>:4:17: 6:6
        StorageDead(_2);                 // scope 0 at <anon>:6:5: 6:6
        goto -> bb0;                     // scope 0 at <anon>:4:5: 6:6
    }
}

fn foo() -> () {
    let mut _0: ();                      // return place in scope 0 at <anon>:1:10: 1:10

    bb0: {
        _0 = const ();                   // scope 0 at <anon>:1:10: 1:12
        return;                          // scope 0 at <anon>:1:12: 1:12
    }
}

You can clearly see that the MIR contains _2 = Lt(const 1_i32, const 2_i32);, which corresponds to 1 < 2.

Another thing is that this lint works at the syntax level, while constant propagation (the optimization that would turn 1 < 2 into true) works at the MIR level. Even if the lint is ported to MIR, it has to run before optimizations (and thus before any const eval) as otherwise while 1 < 2 {} becomes indistinguishable from loop {}.

1 Like

All I'm saying is that just because it doesn't do it now, it is absolutely possible to improve it so it does do that.
BTW, thanks for going into trouble with mir etc. Appreciate it.

1 Like

Of course. And, since the compiler is open-source, everyone can check if this is feasible - including yourself, if you're really determined.

As long as we're talking about compile-time constants, Rust can and does evaluate them in the general case. It already does it for array lengths. It "solves" the halting problem in this case by aborting if the computation doesn't halt quickly enough.

As for the lint, it's probably just simple token match to give style hints to users who use a C idiom, and never meant to be a protection against possibly-infinite loops.

7 Likes

This. A linter is not a static analysis tool. Lints are primarily about code quality and idiomaticity, rather than code correctness.

1 Like

The documentation for this lint confirms this:

Explanation

while true should be replaced with loop . A loop expression is the preferred way to write an infinite loop because it more directly expresses the intent of the loop.

1 Like

I'm quite happy to write loop instead of while true but I cannot buy "more directly expresses the intent" as the reason to prefer "loop".

In fact thinking about neither "while" or "loop" are very satisfying. "while" implies some notion of time which is not a concept the language supports. "loop" implies some kind of cycle which may not be how the compiler chooses to do things.

The missing thing here is the notion of "repetition" which is not stated but instead implied by both "loop" and "while".

The thing should perhaps be called "repeat". Or "rep" as Rust like abbreviations.

If you'd like to have a discussion of the relative merits of different keywords for a particular construct, you might be interested in https://quorumlanguage.com/evidence.html.

But a Rust forum is not the place for that conversation, because Rust isn't going to change its keywords for these things.

2 Likes

To be clear, I'm not advocating changing anything.

Thanks for the link. Interesting.

Also this could happen in a macro expansion and probably shouldn't trigger the lint warning.