1

I'm playing around with building a very simple stack based evaluator in Rust and I've come up against an odd situation where I think the borrow checker is being too conservative:

use std::collections::HashMap;

pub type Value = i32;
pub type Result = std::result::Result<(), Error>;
type Op = Box<dyn Fn(&mut Evaluator) -> Result>;
type OpTable = HashMap<String, Op>;

pub struct Evaluator {
    stack: Vec<Value>,
    ops: OpTable,
}

#[derive(Debug, PartialEq)]
pub enum Error {
    DivisionByZero,
    StackUnderflow,
    UnknownWord,
    InvalidWord,
}

impl Evaluator {
    fn add(&mut self) -> Result {
        if let (Some(x), Some(y)) = (self.stack.pop(), self.stack.pop()) {
            self.stack.push(y + x);
            Ok(())
        } else {
            Err(Error::StackUnderflow)
        }
    }

    fn sub(&mut self) -> Result {
        if let (Some(x), Some(y)) = (self.stack.pop(), self.stack.pop()) {
            self.stack.push(y - x);
            Ok(())
        } else {
            Err(Error::StackUnderflow)
        }
    }

    pub fn new() -> Evaluator {
        let stack: Vec<Value> = vec![];
        let mut ops: OpTable = HashMap::new();

        ops.insert("+".to_string(), Box::new(Evaluator::add));
        ops.insert("-".to_string(), Box::new(Evaluator::sub));

        Evaluator { stack, ops }
    }

    pub fn eval(&mut self, input: &str) -> Result {
        let symbols = input.split_ascii_whitespace().collect::<Vec<_>>();

        // user definition
        if let (Some(&":"), Some(&";")) = (symbols.first(), symbols.last()) {
            if symbols.len() > 3 {
                let statement = symbols[2..symbols.len() - 1].join(" ");
                self.ops.insert(
                    symbols[1].to_string().to_ascii_lowercase(),
                    Box::new(move |caller: &mut Evaluator| caller.exec(&statement)),
                );
                return Ok(());
            } else {
                return Err(Error::InvalidWord);
            }
        }
        self.exec(input)
    }

    fn exec(&mut self, input: &str) -> Result {
        let symbols = input.split_ascii_whitespace().collect::<Vec<_>>();
        for sym in symbols {
            if let Ok(n) = sym.parse::<i32>() {
                self.stack.push(n);
            } else {
                let s = sym.to_ascii_lowercase();
                if let Some(f) = self.ops.get(&s) { // <--------------errors here
                    f(self)?; // <----------------------------|
                } else {
                    return Err(Error::InvalidWord);
                }
            }
        }
        Ok(())
    }
}

fn main() {
    let mut e = Evaluator::new();
    e.eval("1 2 +");
    println!("{:?}", e.stack);
    e.eval(": plus-1 1 + ;");
    e.eval("4  plus-1");
    println!("{:?}", e.stack);
}

I'm getting:

error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> src/main.rs:77:21
   |
76 |                 if let Some(f) = self.ops.get(&s) {
   |                                  -------- immutable borrow occurs here
77 |                     f(self)?;
   |                     -^^^^^^
   |                     |
   |                     mutable borrow occurs here
   |                     immutable borrow later used by call

For more information about this error, try `rustc --explain E0502`.
error: could not compile `evaluator` due to previous error

I believe this is because taking part of the hashmap (f) borrows all of self immutably, then I'm passing self mutably to f(). However, there is no real conflict here (I think).

I'm able to get around this by actually removing and reinserting the value:

    fn exec(&mut self, input: &str) -> Result {
        let symbols = input.split_ascii_whitespace().collect::<Vec<_>>();
        for sym in symbols {
            if let Ok(n) = sym.parse::<i32>() {
                self.stack.push(n);
            } else {
                let s = sym.to_ascii_lowercase();

                if self.ops.contains_key(&s) {
                    let f = self.ops.remove(&s).unwrap();
                    if let Err(e) = f(self) {
                        self.ops.insert(s, f);
                        return Err(e);
                    }
                    self.ops.insert(s, f);
                } else {
                    return Err(Error::InvalidWord);
                }
            }
        }
        Ok(())
    }

But this feels hacky and is a lot more verbose and inefficient. Am I missing something? Is there a way to tell the compiler the first version is ok?

E_net4
  • 27,810
  • 13
  • 101
  • 139
marcantonio
  • 958
  • 10
  • 24
  • 2
    Why don't you make your `Op` take a stack as argument rather than the whole evaluator ? – Denys Séguret Nov 22 '21 at 17:49
  • 3
    What would you expect to happen if `f()` removes all the elements of `ops` (including `f`)? Are you sure that's safe? – Rob Napier Nov 22 '21 at 17:49
  • 2
    Relevant reading: https://stackoverflow.com/questions/47618823/cannot-borrow-as-mutable-because-it-is-also-borrowed-as-immutable As others have pointed out, that code _is not safe_ because the operation could try to modify the operation table that the operation itself lives in. The compiler stopped you rightfully from trying that. Redesigning your operations to depend directly on the arguments of the stack rather than the evaluator would resolve this. – E_net4 Nov 22 '21 at 17:53
  • @DenysSéguret I did that initially but ran into issues with lifetimes and closures. Maybe I missed something there too? Specially, moving references into the user defined closure caused an issue where Box<`dyn Fn`> was being seen as `Box<[closure]>`. It was odd. – marcantonio Nov 22 '21 at 17:54

1 Answers1

2

The compiler is entirely correct, and so is your explanation: The call to get() needs a borrow on self.ops to return an &Op of the same lifetime. You then try to call that FnMut with a mutable borrow of self; this mutable borrow of self aliases with the immutable borrow on self.ops, and in theory it would be possible for an implementation of this FnMut to modify that borrowed Op through self, which is not allowed. The compiler prevented a situation where mutation occurs through an aliased pointer.

This situation often occurs when passing &mut self around, as immutable borrows on members of self which result in more borrows (&self.ops.get() has the same lifetime as &self) "lock" all of self.

While your second example is cumbersome, it is at least correct as proven by the compiler: By removing Op from the hashtable, the FnMut can't reach itself through self anymore, and mutation while aliasing is prevented.

A better approach is usually to avoid taking &mut self as an argument (&mut self as in &mut Executor).

user2722968
  • 13,636
  • 2
  • 46
  • 67
  • Thanks, that makes sense. I will go back to trying to pass in the relevant parts of `self`. I had a weird conflict where Box was being seen as Box<[closure]>. I'll create another question on that if I need to. – marcantonio Nov 22 '21 at 18:36
  • 1
    I guess I need to change the title to "Is there a way to tell rustc that you are wrong even when you think you're right?" :) – marcantonio Nov 22 '21 at 18:38