3

I am reading Mastering Rust. There is an exercise at the end of the first chapter where sample code is provided, and the task is to fix it, iterating by using the generally quite helpful compiler error messages.

I was expecting that the following was an error but it is not:

for line in reader.lines() {
    let line = line.expect("Could not read line.");

For complete context, I have the entire code in a gist. That's the code after I fixed things, and the relevant rows are 37 & 38. However it requires feeding a text file as an argument.


I was expecting an error because line is on the stack (at least the pointer is). Is it right that it can still be destroyed and replaced with no complaint?

What happens under the hood regarding memory management and the stack? I presume that line is actually a reference to a string (a &str type). So, then, this is fine because in either case, the pointer itself - the object on the stack - is just a usize, so that both line objects are of the same size on the stack.

Can I do this with something of a different size? Could the second line have said:

let line: f64 = 3.42;

In this case, the object itself is on the stack, and it is potentially larger than usize.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Mike Williamson
  • 4,915
  • 14
  • 67
  • 104
  • @Stargateur I don't think this is quite a duplicate, because my underlying question isn't *why* this is OK, but *how* this is OK? How do things happen in the stack under the hood to allow this to be possible? Are there simply 2 different objects, one which is the old `line` and another which is the new `line`, even though the old one is no longer accessible? Or is the old one truly overwritten (if in the same scope)? If so, how is that possible for differently sized types? – Mike Williamson May 30 '19 at 04:26

1 Answers1

4

Whenever a variable is declared with let, it's an entirely new variable, separate from anything before it. Even if a variable with the same name already exists, the original variable is shadowed while the new variable is in scope. If a variable is shadowed, it's normally inaccessible.

It's possible to access the value of the old variable in situations where the old variable is still in scope after the new variable falls out of scope, or if the old variable has a Drop implementation.

We can see this in action in the following example.

#[derive(Debug)]
struct DroppedU32(u32);

impl Drop for DroppedU32 {
    fn drop(&mut self) {
        eprintln!("Dropping u32: {}", self.0);
    }
}

fn main() {
    let x = 5;
    dbg!(&x); // the original value
    {
        let x = 7;
        dbg!(&x); // the new value
    }
    dbg!(&x); // the original value again

    let y = DroppedU32(5);
    dbg!(&y); // the original value
    let y = DroppedU32(7);
    dbg!(&y); // the new value

    // down here, when the variables are dropped in
    // reverse order of declaration,
    // the original value is accessed again in the `Drop` impl.
}

(playground)

That's not to say that the original variable is guaranteed to still exist. Compiler optimizations can cause the original variable to be overwritten, especially if the original variable isn't accessed again.

The code

pub fn add_three(x: u32, y: u32, z: u32) -> u32 {
    let x = x + y;
    let x = x + z;
    x
}

compiles to

example::add_three:
        lea     eax, [rdi + rsi]
        add     eax, edx
        ret

If you're like me and aren't too familiar with assembler code, this basically

  1. Adds x and y and puts the result in a variable (call it w).
  2. Adds z to w and overwrites w with the result.
  3. Returns w.

So (besides the input parameters), there's only one variable used, even though we used let x = ... twice. The intermediate result let x = x + y; gets overwritten.

Stargateur
  • 24,473
  • 8
  • 65
  • 91
SCappella
  • 9,534
  • 1
  • 26
  • 35
  • Thank you, @SCappella ! This answers *most* of my confusion, but now I have another. I understand what is happening *logically*, but not what is happening *within memory assignment and Rust's 0-cost abstractions*. In the case where I *shadow but in the same scope*, like your first example (`let x=5;` ... `let x=7`), since it is the same scope, there is no scope where `x=5` anymore. So, in this case, what happens with `let x:i32 = 5` ... `let x:u64 = 7`? My question is: I had a 32-bit item on the stack, which is now a 64-bit item on that same stack position, right? How can I do that? – Mike Williamson May 30 '19 at 04:20
  • 1
    @MikeWilliamson They don't occupy the same stack position. The original variable is made inaccessible, but it still exists unchanged in memory. I'll add an example to show this. – SCappella May 30 '19 at 05:54
  • 3
    @MikeWilliamson The best way to think about it is to regards the inner `x` being an entirely different variable, with a different name. Imagine the compiler even **renaming** it internally (or behaving **as if** it did). Then it's obvious that the two do not share the same stack position, and there is no problem of them having different types. – user4815162342 May 30 '19 at 06:32
  • Great, thanks @SCappella ! BTW, how can you get the bytecode, or what it compiles down to? In Python, I used the `dis` library. Is there something like that in Rust? And if so, how is this managed, since Rust compiles down to particular architectures / OSes. (Will the same program create different bytecode on different platforms? Or is bytecode just an intermediary, like in JVM or Python?) – Mike Williamson May 31 '19 at 17:56
  • 1
    @MikeWilliamson If you want to get assembly using Cargo, see [this question](https://stackoverflow.com/questions/39219961/how-to-get-assembly-output-from-building-with-cargo). If you're ok with online tools, check out the [Compiler Explorer](https://godbolt.org/). [The Rust Playground](https://play.rust-lang.org/) also has options to emit assembly and the other intermediate representations that Rust uses. For your other questions, try asking a new question and I'm sure you'll get a good answer.. – SCappella May 31 '19 at 21:18