Funny beginner bug

In the last weeks, I converted my simple chess engine from Nim to Rust as a beginner exercise. I got it mostly working, including the tiny GTK GUI. What was left until yesterday was the broken transposition table, which I finally fixed by use of some explicit copy operations, which I might later replace by use of references to avoid copy overhead. The bug, which took me a few hours to locate, is related to the following simple example:

struct A {
    a: C,
    b: C,
}

struct C {
    c: Vec<i32>,
}

fn get<'a>(a: &'a A, mut c: &'a C) {
    c = &a.b;
    println!("2: {}", c.c[0]);
}

fn main() {
  
  let a = A{a: C{c: vec![1,2,3]}, b: C{c: vec![4,5,36]}};
  let x = C{c: vec![7,8,9]};
  println!("1: {}", x.c[0]);
  get(&a, &x);
  println!("3: {}", x.c[0]);
}

which prints

1: 7
2: 4
3: 7

while what was desired is

1: 7
2: 4
3: 4

In this example it is not too difficult to see the reason: In fn get() we just locally modify the value of reference c, but not the content. I wonder how often such types of errors may occur in real world programs? For me as a Rust beginner, it was not that easy to find this issue.

PS: A not really related question: How does bright Rust people call source code errors reported by the Rust compiler:

  • Compiler errors
  • Compile errors
  • Compilation errors

Reference: Better don't call source code errors "Compiler errors" · Issue #3785 · rust-lang/book · GitHub

1 Like

"Compiler error" suggests an error in the compiler, that the compiler is doing something wrong rather than a problem your code it is trying to compiler.

"Compile / Compilation error suggests the compiler is finding an error in your code. Either will do for me.

1 Like

well, I wouldn't expect a function named "get" to actually modify it's parameters, and the function type signature indeed only has immutable references.

I guess it depends on your background. for people who came from languages like C++, I don't think it's a problem, especially when you care about const-ness correctly even when you write C++. but programming language apparently have a big influence on programmer's thinking process and mindset. for example, the procedural macro system in rust may feel less capable sometimes for people with a lisp background.

3 Likes

One thing often repeated to beginners is "read the error messages from the compiler". In other languages, they may be very verbose and hard to understand, but Rust really tries to make the compiler your friend. In your example, it will tell you something suspicious about your function:

warning: value passed to `c` is never read
  --> src/main.rs:10:26
   |
10 | fn get<'a>(a: &'a A, mut c: &'a C) {
   |                          ^
   |
   = help: maybe it is overwritten before being read?
   = note: `#[warn(unused_assignments)]` on by default

That points to the location of the bug. It basically means "you never use whatever you pass as the second parameter".

6 Likes

Hi,

I understand your expectation of the output: I would think the same too.

I spent 40 minutes on this... But I could not solve it.

Base on Mutable References, I think x should be mut:

let mut x = C{c: vec![7,8,9]};

My last attempt:

struct A {
    a: C,
    b: C,
}

struct C {
    c: Vec<i32>,
}

//behai
fn get<'a>(a: &'a A, c: &'a mut C) {
    c = &a.b;
    println!("2: {}", c.c[0]);
}

fn main() {
  
  let a = A{a: C{c: vec![1,2,3]}, b: C{c: vec![4,5,36]}};
  //behai
  let mut x = C{c: vec![7,8,9]};
  println!("1: {}", x.c[0]);
  get(&a, &mut x);
  println!("3: {}", x.c[0]);
  
  println!("4: {}", a.a.c[0]);
}

I have compiler error:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/main.rs:12:9
   |
12 |     c = &a.b;
   |         ^^^^ types differ in mutability
   |
   = note: expected mutable reference `&'a mut C`
                      found reference `&C`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` (bin "playground") due to previous error

...I am a beginner too... I do feel I should be able to work it out, but sadly not!

Best regards,

...behai.

1 Like

*c = a.b; works, but you'd need to pass a by value (because you need the ownership to move it into c.

2 Likes

Hi,

Is this what you mean, please?

struct A {
    a: C,
    b: C,
}

struct C {
    c: Vec<i32>,
}

fn get<'a>(a: A, c: &'a mut C) {
    //behai new change
    *c = a.b;
    println!("2: {}", c.c[0]);
}

fn main() {
  
  let a = A{a: C{c: vec![1,2,3]}, b: C{c: vec![4,5,36]}};
  let mut x = C{c: vec![7,8,9]};
  println!("1: {}", x.c[0]);
  //behai new change
  get(a, &mut x);
  println!("3: {}", x.c[0]);
  
  println!("4: {}", a.a.c[0]);
}

There are more errors with this:

   Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `a`
  --> src/main.rs:25:21
   |
18 |   let a = A{a: C{c: vec![1,2,3]}, b: C{c: vec![4,5,36]}};
   |       - move occurs because `a` has type `A`, which does not implement the `Copy` trait
...
22 |   get(a, &mut x);
   |       - value moved here
...
25 |   println!("4: {}", a.a.c[0]);
   |                     ^^^^^ value borrowed here after move
   |
note: consider changing this parameter type in function `get` to borrow instead if owning the value isn't necessary
  --> src/main.rs:11:15
   |
11 | fn get<'a>(a: A, c: &'a mut C) {
   |    ---        ^ this parameter takes ownership of the value
   |    |
   |    in this function

For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` (bin "playground") due to previous error

Thank you and best regads,

...behai.

1 Like

Yes, and then delete the line println!("4: {}", a.a.c[0]);. Or clone inside get.

2 Likes

Hi,

Thank you. It does work :slight_smile:

The output is now:

   Compiling playground v0.0.1 (/playground)
warning: field `a` is never read
 --> src/main.rs:2:5
  |
1 | struct A {
  |        - field in this struct
2 |     a: C,
  |     ^
  |
  = note: `#[warn(dead_code)]` on by default

warning: `playground` (bin "playground") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 2.76s
     Running `target/debug/playground`

Standard Output

1: 7
2: 4
3: 4

Best regards,

...behai.

1 Like

Actually, I came to a very similar issue again. I was going to clean up my already working chess engine, and maybe avoid the clone() call in line rust-chess/src/engine.rs at main · StefanSalewski/rust-chess · GitHub

I had to learn that code like

#[derive(Debug)]
struct S {
    i: i32,
}

fn get<'a>(v: &'a Vec<S>) -> &'a S {
    return &v[0];
}

fn get2<'a>(v: &'a Vec<S>, mut x: &'a S ) -> i32 {
    if v.len() > 0 {
        x = &v[0];
        println!("{:?}", x);
        return 1;
    } else {
        x = &S{i: 0};
        return -1;
    }
}

fn main() {
    let mut v: Vec<S> = vec![];
    v.push(S{i: 1});
    let x = get(&v);
    let mut y: &S = & S{i: 0};
    let n = get2(&v, y);

println!("{}", n);
println!("{:?}", x);
println!("{:?}", y);
}

printing

S { i: 1 }
1
S { i: 1 }
S { i: 0 }

which might result when one tries to convert Wirthian languages with its VAR parameters to Rust will behave differently. So I assume, when I want to avoid clone() calls or other copy operations, I should rewrite function calls with var parameters to Rust functions returning all the mutable data, as option- or result types, or maybe structs or tuples.

#[derive(Debug)]
struct A<'a> {
    b: C<'a>,
}

#[derive(Debug)]
struct C<'a> {
    c: &'a Vec<i32>,
}

fn get<'a>(a: &'a A, c: &mut C<'a>) {
    c.c = &a.b.c;
    println!("2: {}", c.c[0]);
}

fn main() {
    let a = A {
        b: C { c: &vec![4, 5, 36] },
    };
    let mut x = C { c: &vec![7, 8, 9] };
    println!("1: {}", x.c[0]);
    get(&a, &mut x);
    println!("3: {}", x.c[0]);
}
#[derive(Debug)]
struct A {
    b: C,
}

#[derive(Debug, Clone)]
struct C {
    c: Vec<i32>,
}

fn get(a: &A, c: &mut C) {
    c.c = a.b.c.clone();
    println!("2: {}", c.c[0]);
}

pub fn run_funny_beginner_bug() {
    let a = A {
        b: C { c: vec![4, 5, 36] },
    };
    let mut x = C { c: vec![7, 8, 9] };
    println!("1: {}", x.c[0]);
    get(&a, &mut x);
    println!("3: {}", x.c[0]);
}

Yes, we know that both issues are very similar. But for vectors things are even more complicated, because it is not possible to move single elements out of a vector without copying. Note that the solution of BeHai with "*c = a.b;" and my solution in the chess engine with clone() performs a copy operation, which we might want to avoid. Rewriting the function signature and using return values instead of VAR (mutable) parameters seems to be the best option. In C we might use pointer to pointer constructs, so I wonder of nested references like &(& Something) might allow use of mutable parameters without copying as well.

Another point, that I have still to understand is, how moving function local variables into a vector might work. Because function local variables should be stack allocated, and are destroyed when the function terminates. So how can such a variable be moved into a longer living vector, without copy and allocation operation involved?

Always avoid cloning the vec is a good practice.
I provided 2 options, one to use clone, another to use reference.
The reference is actually a pointer.
But keep in mind if you are using reference, be careful with the lifetime scope.

The parameters in a local function should be dropped if they are not the reference.
But when you borrow them, they will not be dropped.
That's what the borrowing rules decide.

1 Like

Thank you very much for your detailed explanations. Currently I am playing with the code below:

#[derive(Debug)]
struct S {
    i: i32,
}

fn push2(v: &mut Vec<S>) {
    let mut s = S{i: 1};
    s.i = 7;
    v.push(s);
    
}

fn main() {
    let mut v: Vec<S> = vec![];
    v.push(S{i: 1});
    push2(&mut v);

println!("{:?}", v);
}

It works, but I wonder if variable s in function push2() is initially stack allocated, and is actually moved into the vector. My guess would be, that s is actually heap allocated, or that it is copied into the vector.

You declared a struct named S which has a known size at compile time because we always know the size of i32. The instance of S should be allocated on stack.

In your main function, you create a mutable vec with element type S.
The vev itself is allocated on stack but it's content or it's elements are allocated on the heap.
Then you push an object of type S into the vec, that's good.
The main function is the owner of the mutable vec.

Then you call the function push2 which takes in one mutable reference of Vec<S> named v, the function push2 does not own the v, because it is just borrowed and when push2 ends, the v will not be dropped.
In the function push2, the mut s is allocated on stack because we always know the size of type S at compile time then it is moved into the mut reference v.
That means now the v has 2 elements. and the s inside the push2 will be dropped after the push2 ends.

1 Like

Thanks again for that detailed explanation. It is mostly the behaviour I expected. The final question is:

the mut s is allocated on stack because we always know the size of type S at compile time then it is moved into the mut reference v.

Is that move operation done without a content copy operation? Because, stack allocated data typically get invalid when the function returns, so I would assume for moving it into the vector, heap data would have to be allocated and the element has to be copied to the newly allocated heap segment? We can call this still a move operation, because it transfers ownership, but it would involve additional an allocation and a content copy operation?

I think this GPT-4 explanation is correct and good:

fn push2(v: &mut Vec<S>) {
    let mut s = S{i: 1};
    s.i = 7;
    v.push(s);
    
}

it's like saying create a new S with the modified value and push it into the Vec even though, in practice, Rust is more efficient and directly transfers the ownership and memory contents without creating a separate new instance.
That's correct the contents of s are indeed copied to the heap (where the vector resides), but this is a shallow copy, the type S is actually simple and no need for any heap-allocated.
So in summary, there is actual a copy here but not the copy we mentioned in rust ownership.

1 Like