Skip to content

Instantly share code, notes, and snippets.

@xpepper
Created April 19, 2024 21:39
Show Gist options
  • Save xpepper/ab57803206872ed4aedab067cc3d089d to your computer and use it in GitHub Desktop.
Save xpepper/ab57803206872ed4aedab067cc3d089d to your computer and use it in GitHub Desktop.
practicing ownership and borrowing (`mixtral-8x7b-instruct-v0.1`)

Sure, I'd be happy to help you practice recognizing ownership and borrowing violations in Rust! Here are some code examples for you to evaluate:

Example 1:

let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2);

let r3 = &mut s;
println!("{}", r3);

Would this code compile? Why or why not?

Example 2:

fn main() {
    let s = String::from("hello");

    let r1 = &s;
    let r2 = &s;

    println!("{}, {}", r1, r2);

    takes_ownership(s);
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}

Would this code compile? Why or why not?

Example 3:

fn main() {
    let s = String::from("hello");

    let r1 = &s;
    let r2 = &s;

    println!("{}, {}", r1, r2);

    changes_contents(&mut s);

    println!("{}", s);
}

fn changes_contents(some_string: &mut String) {
    some_string.push_str(", world");
}

Would this code compile? Why or why not?

Example 4:

fn main() {
    let s = String::from("hello");

    let r1 = &s;
    let r2 = &s;

    println!("{}, {}", r1, r2);

    let r3 = &mut s;
    let r4 = &mut s;

    println!("{}, {}", r3, r4);
}

Would this code compile? Why or why not?

Answers:

Example 1: No, this code will not compile because r3 is a mutable reference, but there are already immutable references r1 and r2 in scope. Rust does not allow mutable references to coexist with immutable references.

Example 2: No, this code will not compile because takes_ownership function takes ownership of s, but r1 and r2 are still trying to borrow s as an immutable reference. Rust does not allow borrowing after ownership has been moved.

Example 3: Yes, this code will compile. Although changes_contents takes a mutable reference, it does not modify the original s string. Instead, it modifies the some_string parameter, which is a different variable that happens to have the same value as s.

Example 4: No, this code will not compile because there are multiple mutable references r3 and r4 to the same variable s in scope. Rust does not allow multiple mutable references to the same variable.

@xpepper
Copy link
Author

xpepper commented Apr 19, 2024

produced with https://chat.lmsys.org/ and the following prompt:

I'm trying to learn ownership and borrowing in Rust programming language, and I would like you to help me practicing in recognizing code that violates Rust rules on ownership and borrowing, by showing me some code examples and then let me choose, for each example, if it would compile or not (and why it should not compile).

I'm going to give you same examples, please use them to have an idea of what kind of code example you could show me, but don't just copy those examples, try to come out with your own examples.

For example you could show me a code example like this

1 let s1 = String::from("hello");
2 let s2 = s1;
3 
4 println!("{}, world!", s1);

And I would have to recognize that this code does not compile because at line 4 s1 would be an invalid reference, since ownership moved to s2.

Another example you could show me would be something like this

fn main() {
    let s = String::from("hello");  

    takes_ownership(s);             
                                    
    let x = 5;                      

    makes_copy(x);                  

} 
  

fn takes_ownership(some_string: String) { 
    println!("{}", some_string);
} 
  

fn makes_copy(some_integer: i32) { 
    println!("{}", some_integer);
} 

Or this, for help me understand borrowing

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

Or this one

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

@xpepper
Copy link
Author

xpepper commented Apr 20, 2024

BTW all models I tried fail at giving the right answer at the first shot for this Rust code

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{}, {}", r1, r2);

    let r3 = &mut s;
    println!("{}", r3);
}

This code compiles, but all models I tried (GPT4Turbo, Claude Opus, GeminiPro, Perplexity) all say that it won't :D
When you gave the right answer, they correct their response and also resonate correctly about why it is indeed so

My apologies, you are absolutely correct! The provided Rust code snippet does compile due to a concept called Non-Lexical Lifetimes (NLL). I should have taken that into account before explaining why the code wouldn't compile.

Explanation with Non-Lexical Lifetimes (NLL):

NLL is a feature in Rust that allows the compiler to perform a more fine-grained analysis of lifetimes, specifically focusing on the actual usage of references rather than just their lexical scope.

In the given code:

let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2);

let r3 = &mut s;
println!("{}", r3);

Even though r1 and r2 are declared before r3, the key point is that their last usage is within the println! statement before r3 is created. Due to NLL, the compiler understands that r1 and r2 are no longer needed after the println! and their lifetimes do not overlap with the lifetime of the mutable reference r3. Hence, the code compiles without any issues.

Benefits of NLL:

  • More flexible borrowing: NLL allows for more relaxed rules regarding lifetimes, making it easier to write code without running into borrowing errors unnecessarily.
  • Reduced need for workarounds: Prior to NLL, developers often had to resort to workarounds like creating artificial scopes or using unsafe code to satisfy the borrow checker. NLL eliminates the need for such workarounds in many cases.

Summary:

The initial explanation was based on an older understanding of Rust's borrow checker. With NLL, the compiler is smarter about tracking lifetimes, allowing the code to compile successfully. This demonstrates the continuous evolution of Rust and its focus on improving developer experience while maintaining memory safety.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment