I'm really surprised that this compiles

fn main() {
    let arr_0: [u32; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

    let ptr_0 = Box::new(arr_0);

    let v = ptr_0[11];
    println!("v:{}", v);
}

Clearly, it could be checked during compilation and instead of runtime we would have compile time error?

unconditional_panic only checks the simplest case. Going beyond that is time consuming to deduce, and it's hard to draw a line where it stops.

3 Likes

But that is somewhat contrary to (correctly doesn't compile):

fn main() {
    let mut arr_0: [u32; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

    let ptr_0 = Box::new(arr_0);

    let ptr_v = ptr_0[11];
    let arr_v = arr_0[11];//Doesn't compile
    println!("v:{}", ptr_v);
}

And AFAIK deref rules should kick in during calling []operator on Box and identical behavior (to what we see with the array) should be observed.
That's why I'm saying that this should not compile.

It would be a reasonable extension to the lint, sure.

It seems only arrays are supported, not slices:

[0u8][1] // error
(&[0u8])[1] // compiles

However the compiler (I guess LLVM but not rustc?) is able to do pointer arithmetic checks to enable some optimizations, so I guess it would be possible to check the slice length, especially in this trivial case.

I'm doubtful whether extending the lint to "see" through Deref would be very helpful - the original code snippet doesn't feel like something you would see outside of the Rust compiler's test suite.

The vast majority of code I've seen will use iterators or indices that have been determined using business logic at runtime, so there aren't many places where you will use a hard-coded index into an array type who's length is known at compile time to begin with.

2 Likes

However true it may be, it is incorrect from the orthogonality point of view that this code doesn't compile when array is used and compiles when Box is used.
However rarely such scenarios happen, they should simply behave identical, because the rules about deref (ttbomk) say so.
Or are we interested in the compile time errors only for code that is only used in vast majority, and the rare usages are of no interest to us?

Not extension to lint but fix in the compiler if anything, so those two give identical results.

Unfortunately, if we are going into this route, the "correct" way to do it would be never throw compiler error.

Can you provide source for this claim about the rules? I doubt there's a rule saying that.

Edit: Just in case you misunderstand this again: I mean it's hard to decide which cases are we going to cover.

For example:

fn main() {
   let k = [0; 10];
   k[9usize + 1];   // This results compile error.
   k[9usize.wrapping_add(1)]; // But this does not.

   const C: usize = 9usize.wrapping_add(1);
   k[C];  // This errors again
}

You can easily see it's just checked for the simplest of the simplest cases. I won't be surprised if Rust adds more checks. But I won't call the current behavior incorrect either. I would say any reasonable checks are welcome, and perfection should not be an enemy of good.

To clear up possible misunderstandings: We are talking about a lint. Even though the compiler output says “error:” this is not a true compilation error, but instead just a lint, i.e. the kind of thing that’s generally responsible for many of the helpful warning:s you’ll get when using Rust. There are some “deny-by-default” lints, which means that the lint level is set to deny by default, something that’s also possible manually with any other lint. For example

#![deny(unused_variables)]

fn main() {
    let x = 1;
}

will output

error: unused variable: `x`
 --> src/main.rs:4:9
  |
4 |     let x = 1;
  |         ^ help: if this is intentional, prefix it with an underscore: `_x`
  |
note: the lint level is defined here
 --> src/main.rs:1:9
  |
1 | #![deny(unused_variables)]
  |         ^^^^^^^^^^^^^^^^

and not compile successfully. But of course, this code is not actually suffering from a true/hard compilation error.

Lints are generally acting on a best-effort basis (which explains the somewhat inconsistently triggering lint for different array-like types that you are experiencing here), they can have (though usually shouldn’t, if they are enabled by default) false positives, and there will very commonly be “false negatives” in the form of “equally bad” situations not covered by the lint. The case you are running ito could be considered a form of “false negative”. Another example could be something like

#![deny(unused_variables)]

fn main() {
    let x = 1;
    drop(x);
}

which will no longer lint, even though using an integer via drop arguably is not much of an actual usage at all.

The reason why some lints are deny-by-default is that the code they fire on is considered to be so bad or buggy that building and/or running is commonly not considered worth the effort. Still, you can change the behavior if you want. If you degrade it to a warning, then

#![warn(unconditional_panic)]
fn main() {
    let mut arr_0: [u32; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

    let ptr_0 = Box::new(arr_0);

    let ptr_v = ptr_0[11];
    let arr_v = arr_0[11];//warns
    println!("v:{}", ptr_v);
}

will, too, compile just fine.

Another way in which deny-by-default lints differ from errors is that they will not make compilation fail if they happen in any dependency of your main crate. Cargo achieves this behavior by applying lint capping, silencing all (well… at least most) warnings (and errors from otherwise denied lints), because the person compiling the code is assumed not to be the developer of the dependency in question. This behavior also ensures that changes to the Rust compiler regarding lint behavior can happen without breakage. Rust can even introduce “error:”s from deny-by-default lints without all that much breakage, because such changes will not prevent any dependent crates from building, and even the crate itself can still be build without any source code changes e.g. by using the --cap-lints flag appropriately.

9 Likes

Yes, from the official docs:

specifically that line:
" Implementing Deref for smart pointers makes accessing the data behind them convenient,"

Which is as if we were accessing the data directly. Which in turns means that either it should fail to compile for both situations or none.

This is good answer, unfortunately not to my question :wink:
My point is clear:
Either both situations should fail or none of them. Having one compile and the other fail makes it inconsistent.

I think that everyone here understands that this is your position, they just disagree that it's a useful guideline. If it were applied consistently to all similar situations, it would effectively lead to the removal of the entire lint suite.

That would be a net-negative for the development experience, which implies that your guiding principle of consistency may not be the most valuable one to follow. In its place, I propose a different time-honored principle, to "not let the perfect be the enemy of the good," by providing what helpful guidance we can to developers, even if similar guidance can't be given in other similar situations.

9 Likes

No. It doesn't means that.

1 Like

It does.

1 Like

Perhaps differently:
Do you expect line marked as A to have identical result to line marked as B or not (I'm talking here with regards to Deref activated on line A)?

let mut arr_0: [u32; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

    let ptr_0 = Box::new(arr_0);

    let ptr_v = ptr_0[0];// LINE A
    let arr_v = arr_0[0];// LINE B

What's your goal from this thread? What's the real code that made you open it?

Because, as others have said, "all lints are perfect and catch every situation" is not practical. Indeed, it's not even possible: Rice's theorem - Wikipedia

4 Likes

It doesn't really matter what's "the real" code. What matters is that we have inconsistency that shouldn't be there. Instead of compile time error we have run time. For identical situations.
You have "the real" code that prompt me to open it in my OP btw.

To repeat @steffahn, this is not a "compile time error"; it's a compile-time lint, that just happens to be particularly insistent. Some people find it helpful when the compiler does a minimal bit of work to find unintentional typos and whatnot that would do nothing but cause a panic at runtime. It's impossible to avoid every minor inconsistency in this detection, since every additional feature makes the compiler take longer to run. (You call the situations "identical", but this is hyperbole: even though they share the same syntax, they perform different operations from the compiler's point of view.)

If you do not find this best-effort behavior helpful, you can add #![warn(unconditional_panic)] or #![allow(unconditional_panic)], and the compiler will happily make both expressions panic at runtime.

1 Like

Deref has an impact on runtime behavior. At runtime - yes, they are identical. For compile-time analysis - they aren't, and Deref never actually promised that (neither it can, since actual Deref implementation can be arbitrarily complex).

2 Likes