2
votes

Background:

I am trying to implement a simple Virtual Machine in Rust. For now, I am working on a "RegisterBank" which supports space for Strings and Integers. The RegisterBank struct looks the following:

pub struct RegisterBank {
    int_registers: Vec<i32>,
    str_registers: Vec<String>,
}

So it is just a simple collection of two vectors.

Previous work:

When I tried to implement the "load" and "store" functions, there was the choice between two seperate functions

pub fn load_int(...) { ... }
pub fn load_str(...) { ... }

and pattern matching (which I wanted to learn anyway)

pub fn load(self, register: SomeMatchableType) {
    match register { ... }
}

Because having one function for two very similar tasks seemed good, I tried something like this:

enum OperandType {
    Number(i32),
    Word(String),
}

and then have a function like pub fn load(self, register: OperandType) which matches register and returns either a String (or &str, whatever) or an integer depending on its operand type.

Problem: Currently the implementation is stuck on two seperate functions (for i32 and String) and this is working fine. Since I have managed to do this for fn store(&mut self, register: usize, value: OperandType) this should be possible for fn load(self, register: ???) as well. My biggest problem is to design such a function that combines both tasks into one pattern match depending on the OperandType enum (or maybe something else if someone has a clever idea).

Basically, the solution should do:

  1. Decide which register (int_registers or str_register) to choose based on input parameter
  2. Get the content of the register
  3. Return it
1
You could make a enum RegisterType { Number(usize), Word(usize) } - Adrian
BTW this doesn't really have anything to do with your question but I think your code could look more idiomatic if you renamed OperandType to just Operand - Adrian

1 Answers

4
votes

It is not possible for load() to pattern-match on an input parameter because as defined it simply doesn't have one that could be matched. What you seem to really be looking for is a way to make load() generic by return type — and you are in luck because Rust actually makes that possible.

You need to create a generic trait and provide implementations for the types you are covering, in your casei32 and String:

trait LoadFromRegister<T> {
    fn load(&self, register: usize) -> T;
}

impl LoadFromRegister<i32> for RegisterBank {
    fn load(&self, register: usize) -> i32 {
        self.int_registers[register]
    }
}

impl LoadFromRegister<String> for RegisterBank {
    fn load(&self, register: usize) -> String {
        self.str_registers[register].clone()
    }
}

load can be called either by providing a return type in the context or explicitly using the starfish operator:

let intreg: i32 = rb.load(2);  // or, let intreg = rb.load::<i32>(2);
let strreg: String = rb.load(2);

Similar technique can be used to get rid of the clunky OperandType wrapper. Define a StoreToRegister trait that provides store() and implement it for RegisterBank with i32 and String value types doing the concrete stores:

trait StoreToRegister<T> {
    fn store(&mut self, register: usize, value: T);
}

impl StoreToRegister<i32> for RegisterBank {
    fn store(&mut self, register: usize, n: i32) {
        self.int_registers[register] = n;
    }
}

impl StoreToRegister<String> for RegisterBank {
    fn store(&mut self, register: usize, s: String) {
        self.str_registers[register] = s;
    }
}

This provides the closest Rust equivalent to argument overloading, allowing main() to look like this:

fn main() {
    let mut rb = RegisterBank::new();
    rb.store(2, 5);
    rb.store(2, "foo".to_owned());
    let intreg: i32 = rb.load(2);
    let strreg: String = rb.load(2);
    assert!(intreg == 5);
    assert!(strreg == "foo");
}

The two traits can be merged into a single RegisterStorage trait that provides both load and save. Complete code at the playground.