0

Can anyone offer a suggestion as to why the phenomena below is occurring? To me it seemed to be the sort of thing that the Rust compiler would catch.

use std::io;
  fn main() {
     let mut guess = String::new();
      loop {
         io::stdin().read_line(&mut guess).expect("Failed to read line");
          let guess:u32 = guess.trim().parse().expect("Please type a number!");
         println!("{}", guess);
      }
  }

The above compiles which I expected it NOT to compile. This is the issue I'm trying to resolve. Below is the output while running the app. It crashes after second entry.

10
10
20
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', guess.rs:8:46
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

result of running the above.

Here is the above code with the loop removed. Instead of the loop the same calls are being made - but two sets instead of a loop. This is to mirror what happened in the loop. This fails as I expected.

 use std::io;

 fn main() {
     let mut guess = String::new();

     io::stdin().read_line(&mut guess).expect("Failed to read line");

     // below represents what would have occurred in the loop.
     let guess:u32 = guess.trim().parse().expect("Please type a number!");
     println!("{}", guess);

     let guess:u32 = guess.trim().parse().expect("Please type a number!");
     println!("{}", guess);
 } 

below is the compiler error which I expected to occur at compile time for the code above.

error[E0599]: no method named `trim` found for type `u32` in the current scope
  --> guess2.rs:12:31
   |
12 |         let guess:u32 = guess.trim().parse().expect("Please type a number!");
   |                               ^^^^ method not found in `u32`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0599`.

The problem: when in a loop - it passes compilation - I expect that it should not pass compilation and should raise the same compilation error as if there were no loop.

KitakamiKyle
  • 83
  • 1
  • 10
  • Note that if you rename the integer version of `guess` to `guessInt` (i.e. get rid of the shadowing) in the loop version, it will change nothing - you'll still get the same panic (which should have already told you that your problem doesn't actually have anything to do with shadowing). If you do the same thing in the unrolled version, the compilation error will go away and you'll get the same panic as in the loop version. – sepp2k Jun 23 '21 at 11:40
  • sepp2k, I'm beginning to see that shadowing was an overstatement on my part. Looking back - I would have rephrased that. However, because the type is being changed - it appears that the compiler isn't looking forward to usages in the loop to make sure the types agree. But, in the unrolled version - it does catch it. If I could rephrase my title and overview statement - it would be that the types not agreeing in a loop aren't getting caught by the compiler. Are we saying the same thing here? – KitakamiKyle Jun 23 '21 at 11:51
  • 1
    That's not what's happening at all. If shadowing the variable actually changed the scope (beyond the scope of the new variable), renaming the variable would fix the issue, wouldn't it. As explained in the answers, the issue is that you're appending to the string. That's the only issue - the shadowing has nothing to do with it. – sepp2k Jun 23 '21 at 11:53
  • @sepp2k, after reviewing the Lukas's explanation - I think I finally understand why you were saying that the shadowing is irrelevant. I thought it was because the type had changed in the loop - AND - I thought the type was remaining changed until the loop completed all iterations. But, instead it reverts back to the original instance / in memory at the beginning of each loop and therefore garbage on top of garbage ~ something to this effect. Am I understanding better? – KitakamiKyle Jun 23 '21 at 12:21
  • Yes, except I wouldn't call it "garbage on top of garbage", more like "the new user input on top of the old user input". And again I want to encourage you to rename the variable to remove the shadowing [like this](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f9c236183cbb2bb66f411c56ce28a803). As you can see, the problem still persists without `guess` ever having a different type in any scope and you can think of the problem without thinking about anything changing its type and then changing back. – sepp2k Jun 23 '21 at 12:29
  • Thanks @sepp2k, yes - I understand loud and clear the irrelevance of the shadowing. I looked at your playground code as well - thank you for taking the time to make that. Generally c, c++ problems like this are easy for me - I don't know why it took so long to grasp this one. I'm still fairly new overall with Rust. Thanks again. – KitakamiKyle Jun 23 '21 at 12:40

2 Answers2

4

Let me explain what's going on here. The whole thing is actually not trivial!

Before we dive into your code, let's take a look at this tiny example to understand how "variable shadowing" works (Playground):

let foo = "peter";
if true {
    let foo = 27;
    println!("{}", foo);
}

println!("{}", foo);

This outputs:

27
peter

Why? In the inner scope, the variable foo is shadowed. But that doesn't mean that the original variable is removed or replaced. It remains valid, but is simply inaccessible due to the other, shadowing, variable. Once the inner scope ends, the inner foo is gone, and the outer foo is accessible again.


With that out of the way, let's take a look at your code but modified:

let mut guess = String::new();

// Run exactly twice!
for _ in 0..2 {
    println!("{}", guess); // A

    io::stdin().read_line(&mut guess).expect("Failed to read line");
    println!("{}", guess); // B

    let guess:u32 = guess.trim().parse().expect("Please type a number!");
    println!("{}", guess); // C
}

What would that print? First, at position A, an empty string, because guess was just created empty. Then, at position B, whatever was read from stdin (i.e. whatever you typed in your terminal). Let's say you wrote 27. And at position C, guess is shadowed and now prints the parsed value, e.g. 27.

But then the loop repeats once. So the println at position A is executed again. This prints "27" again! The integer variable guess does not exist at that point, right? It's only created further down. So it does not shadow the string variable guess. And the string variable was not recreated or deleted whatsoever. It kept the same value it had at position B in the previous iteration.

And this is the bug in your code: read_line now appends your new input to the string. Say you now wrote 66 in stdin. Then guess will now contain "27\n66\n". Then you say guess.trim().parse(), but "27\n66" cannot be parsed as an integer, thus parse returns Err. And since you call expect on it, it panics.

The solution is to call guess.clear() right before read_line in every loop iteration to make sure the string is empty before reading into it.


Why does the compiler show an error when you manually unroll the loop?

Well, this is just a coincidence actually. So it's not like the compiler can generally catch fewer errors when you use loops.

let mut guess = String::new();

io::stdin().read_line(&mut guess).expect("Failed to read line");
let guess:u32 = guess.trim().parse().expect("Please type a number!");

io::stdin().read_line(&mut guess).expect("Failed to read line");
let guess:u32 = guess.trim().parse().expect("Please type a number!");

The difference here is that the integer guess is still in scope in your "second iteration". That's the difference to the loop solution. And well then, read_line(&mut guess) is a simple type error because read_line expects a string but gets an integer.

So the compiler doesn't really point out the logic error to you, but points to something more or less unrelated. This might just happen to make you find the logic error.


Maybe this helps: variable shadowing operates on the scopes of your program code and NOT on the control flow graph of your program. Shadowed variables are not deleted or replaced: they are simply inaccessible for some scope, but can become accessible again at some other point.

Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
  • Lukas - I'm still reading thru your explanation - thanks for the depth of your answer. This statement of yours has got me thinking. ```The integer variable guess does not exist at that point, right?``` Are you saying that at the beginning of each iteration - the loops internal shadowed value got dropped and the outer String guess is now active again at the beginning of each loop iteration?. If this is what i'm missing then I get it now. – KitakamiKyle Jun 23 '21 at 12:04
  • @user2281135 You're welcome! Regarding the question: yes, the integer variable `guess` is dropped at the end of each loop iteration. Just like the integer `foo` is dropped at the end of the `if` in my first example. (And just in case you're wondering: this is what happens semantically. Dropping an integer is a noop and the code will be well optimized. But you don't need to worry about machine code the compiler produces. The semantics are important here) – Lukas Kalbertodt Jun 23 '21 at 13:36
2

The issue has nothing to do with shadowing. Which incidentally is why you should at the very least post error messages (whether compiler or runtime errors) when you have one, and ideally complete reproduction cases as small as you can make them.

In the repro case above, the error I get is:

ParseIntError { kind: InvalidDigit }

This is a typical misunderstanding of read_line: as documented, read_line will

read bytes from the underlying stream until the newline delimiter (the 0xA byte) or EOF is found. Once found, all bytes up to, and including, the delimiter (if found) will be appended to buf.

This means on the first loop around, it reads, say, 6\n, appends that to the buffer, the next lines trims the contents getting 6 and parses it to an integer.

On the second loop, it reads, say, 8\n, and appends that to the buffer in which the previous line is still present therefore the buffer now contains

6\n8\n

trimming will yield 6\n8. Since \n is not a valid digit, FromStr returns an error.

E_net4
  • 27,810
  • 13
  • 101
  • 139
Masklinn
  • 34,759
  • 3
  • 38
  • 57