How does variables inside a local scope work? Do they get popped off the stack?

One of the things that I don't know how it work are local scopes inside functions. From what I know, when a function is called, it gets pushed onto the stack with all its data and when its scope is over, it gets popped off the stack. But what happens with inner local scopes inside a function? Let's consider the following code:

fn main() {
  let x = 10; // It will live till the end of its scope

  { // The local scope starts here
    // This variable will also live until the end of its scope
    // but the end of its scope will not be the end of the function
    // so what happens with memory? Is this data part of
    // block that is reserved for the function? So even tho we
    // we cannot use it, its there in memory. Or does Rust push
    // another block for every local scope that is created and then
    // pops it off the stack when it's over?
    let local_x = 10;
  } // The local scope ends here
}

I think the comments explained my question. For any further clarification, I would be glad to add a note.

Nothing really happens when it goes out of scope unless it has a destructor. If it has a destructor, it inserts a function call to the destructor, then does nothing.

When you enter the main function, this gives you an amount of stack space. You can think of it as a byte array of some length chosen by the compiler. During compilation the compiler decides where in this array the variables would go, for example in your case it might choose to get a stack frame of length 8 and assign it like this:

  • Bytes 0 to 4 contain x
  • Bytes 4 to 8 contain local_x

At this point, any accesses of a local variable are hard-coded in the assembly to use the appropriate subset of the stack frame to store that variable. When local_x goes out of scope, the compiler doesn't do anything about that — it just leaves the memory in bytes 4 to 8 as they were and doesn't read them again. The stack memory containing local_x is deallocated when the function returns in a way that deallocates the entire stack frame at once.

I will add that in cases like this:

fn main() {
    let x = 10;
    {
        let y = 20;
    }
    {
        let z = 30;
    }
}

The assignment of the stack frame might be this:

  • Bytes 0 to 4 contain x
  • Bytes 4 to 8 contain y
  • Bytes 4 to 8 contain z

That y and z overlap is fine because you don't need them at the same time. I will add that the compiler is perfectly able to see that x is not used after the creation of y, so it could easily make all three share the same byte range in the stack frame.

3 Likes

Perfectly explained! Thanks a lot for your time and I wish you an amazing day!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.