Why am I able to mutate a string literal?!

While learning Rust lang, I came across this sentence in the Rust book - "String literals are convenient, but they aren’t suitable for every situation in which we may want to use text. One reason is that they’re immutable."

fn main () {
    let mut s = "hello";
    s = "hey";
    println! ("{}", s);
}

Output:
hey

Why am I encountering this kind of a behaviour?

1 Like

You aren't changing the string literal itself, you're simply changing which string literal s refers to. E.g. you can't add more text to hello like you would be able to with a String

fn main() {
    let mut s = "Hello".to_string();
    s.push_str(" world!");
    println!("{}", s);
}

playground

2 Likes

Isn't that only because push_str is only available for String and not String literals?
And, isn't changing value of s from "hello" to "hey" mutating it?

It might help if we write out the type explicitly.

let mut s: &str = "hello"

The variable s is a mutable reference to a str. The reference can be mutated to point to another str but the str itself never changes.

2 Likes

Yep, you are mutating s, but s is just a pointer to a string. You are changing the pointer, not the string it points at.

Then how would you try to mutate s and not just change the pointer?

You can't.

I tried this--

let mut s: &str = "hello";
s = "hey";
println! ("{}", s);

Output:
hey

Yeah, it's the same as your original code, except the type of s is explicit.

Hm. It's a bit hard to wrap my mind around it..

When you make a string literal, the characters are placed somewhere in your executable. This position has a memory location, and the variable s really only just contains an integer that describes this memory location. When you declare s as mutable, that allows you to change the integer to some other location in memory, but the actual data stored at that location in memory can't be changed.

6 Likes

Consider this example:

fn main() {
    // Create two strings pointing to the same memory location.
    let mut s = "Hello";
    let s2 = s;
    
    // This does not change s2.
    s = "hey";
    println!("{}", s2); // prints Hello
}

Note that assigning s2 to s does not make a copy of the string — they point to the same string in memory.

9 Likes

In rust you can have a mutable value, an immutable value, a mutable reference to an immutable value or a mutable reference to a mutable value. A string literal is immutable value, let mut s = "hello" creates a mutable reference to an immutable value, which allows you to change what s refers to (which is what you do in the next line of your code).

3 Likes

You've just made the reference to the string mutable, not the string itself. For example:

fn main() {
    let mut s = "hello";
    println!("{}", s);
    s = "there";
    println!("{}", s);
}

This modified s (the reference to "hello"), it didn't modify the hello string itself. You can prove this adding an additional reference to the original string:

fn main() {
    let s1 = "hello";
    let mut s = s1;
    println!("{}", s);
    s = "there";
    println!("{} {}", s1, s);
}

Consider this piece of code:

let mut number = 3;
number = 4;
println!("{}", number);

This prints 4. Would you also say that in this case, you mutated 3 and it became 4? Of course not. You mutated the contents of the number variable.

Changing the contents of a variable doesn't magically change whatever it was initialized with in the first place. The initializer value is a separate entity from the contents of the variable. It is moved (or copied, if possible) into the storage represented by the variable upon initialization or assignment. It can even cease existing afterwards. There's nothing special about string literals here.

4 Likes

I would like to mention that there's a bit of language that's being misused and could become ambiguous given how string literals are handled:

A binding is the name given to a variable on the stack. Having a let mut var means that your var on the stack is mutable. Having let var means that you have a value of whatever type var is on the stack, and you will refer to it as var from now on.

A reference is a pointer to a value somewhere in memory which gets tracked by the compiler at compile time. It has what's called a lifetime which is how long the value it points to will live for. The lifetime is the lifetime of a binding. As soon as a binding disappears, the lifetime ends and therefore the compiler must make sure that you don't end up with a reference to a dead binding.

The mutability of a reference tells you if it's okay to mutate the value under the pointer.
The mutability of a binding only serves as a lint.

The difference between a binding and a reference is important since string literals are stored under references to the point in memory where the string actually is, as @alice mentioned.

2 Likes

@niharrs It seems you are confused about pointers. Don't worry, you are not alone! Let's try to visualize it a bit more (Playground):

fn main()
{
   // These are references to strings, so the value of
   // the text isn't stored in them. Just the address of
   // where in memory the string data is stored. 
   // This is a fat pointer, containing 2 usizes, 
   // the memory address of the first character and the length
   // of the string (slice).
   //
   // The actual string data for a string literal (hardcoded in
   // the source code) will be in the data segment of the process.
   // This is a read-only area in memory with data that is 
   // hardcoded in the binary.
   //
   let mut x: &str = "hello";
   let     y: &str = "world";
   
   // Let's find out where in the processes memory this string data
   // lives. It's phone number if you want. Where can we find this
   // string?
   //
   let x_addr = x.as_ptr() as usize;
   let y_addr = y.as_ptr() as usize;
   
   // This line will print something like:
   // Address of x (hello) = 0x55a36c3486d0
   //
   // Rust string format even has a special shorthand for this,
   // this will print exactly the same as the first print below:
   // println!( "Address of x ({}) = {:p}", x, x );
   // 
   println!( "Address of x ({}) = 0x{:2x}", x, x_addr );
   
   // Address of y (world) = 0x55a36c3486d5
   // 
   // Note how the compiler layed them out exactly one after the other.
   // Five characters for "hello" and then starts "world".
   // It could have been somewhere else, but why not just put one
   // after the other.
   // 
   // So if you would read 10 bytes from starting from the address of x,
   // you would get the string "helloworld".
   //
   // Also note that these addresses will change everytime you run the 
   // process, as these are not physical addresses into the computers
   // memory, but rather into a virtual address space which also is 
   // randomized for security reasons. This randomization actually
   // depends on the operating system running the process, not the
   // compiler.
   //
   println!( "Address of y ({}) = 0x{:2x}", y, y_addr );
   
   // So let's mutate:
   //
   x = y;
   
   // Where is x pointing now?
   // 
   // Address of x (world) = 0x55a36c3486d5
   // 
   // Yep, it's pointing at "world".
   //
   println!( "Address of x ({}) = {:p}", x, x );
   
   // If x is pointing at "world", then what the hell
   // is at 0x55a36c3486d0 ?
   // We still have x_addr, which we didn't change. It's 
   // just a usize. Let's turn it into a string:
   // 
   // WARNING: the following is undefined behavior. I'm just
   // going by the fact that in the playground, everytime I 
   // ran this, "world" was stored directly after "hello".
   // 
   // There is absolutely no guarantee this is the case, 
   // so we will put an assert... Otherwise this might segfault.
   //
   assert!( x_addr+5 == y_addr );
   
   let bytes: &[u8]        = unsafe { std::slice::from_raw_parts( x_addr as *const u8, 10 ) };
   let new  : &'static str = std::str::from_utf8( bytes ).expect( "valid UTF" );
   
   // OMG, we did it. It's all still there, exactly where we
   // put it. No strings were mutilated in the making of this program. 
   //
   // Address of new (helloworld) = 0x55a36c3486d0
   //
   println!( "Address of new ({}) = {:p}", new, new );
}
4 Likes

Here's an example of trying to mutate a str that works, because its taken from a mutable String, not a str literal:

fn main() {
        let mut source: String = "hello".to_string();
        let s: &mut str = &mut source;
        s.make_ascii_uppercase();
        println!("{}", s);
}

playground link

And here's the same code, but where the s is a string literal instead. This one won't compile.

fn main() {
        let mut s = "hello";
        s.make_ascii_uppercase();
        println!("{}", s);
}

playground link

Note that there's nothing magical going on with string literals. The s in the second example is a mutable binding to a normal immutable &str, and string literals don't provide a capability to get back an &mut str (the full assignment is let mut s: &str = "hello"). The same would be true of taking a str reference to any immutable memory location (like if we had dropped the mut from let mut source.

Whereas with the example of a str that is a reference to a mutable String (the first playground link), we can even have an immutable binding, as long as the reference itself is mutable. (let s: &mut str = &mut source). The mut is in a different place in the assignment