Type Conversion

The previous section introduced three string types in the Value type. When creating a string type, different types need to be generated according to the length. This judgment should not be handed over to the caller, but should be done automatically. For example, the existing statement:

     self. add_const(Value::String(var));

should be changed to:

     self.add_const(str_to_value(var));

The str_to_value() function converts the string var into the string type corresponding to Value.

From trait

This function of converting (or generating) from one type to another is very common, so the From and Into traits are defined in the Rust standard library for this. These two operations are opposite to each other, and generally only need to implement From. The following implements the conversion of the string String type to Value type:

impl From<String> for Value {
     fn from(s: String) -> Self {
         let len = s.len();
         if len <= SHORT_STR_MAX {
             // A string with a length of [0-14]
             let mut buf = [0; SHORT_STR_MAX];
             buf[..len].copy_from_slice(s.as_bytes());
             Value::ShortStr(len as u8, buf)

         } else if len <= MID_STR_MAX {
             // A string with a length of [15-47]
             let mut buf = [0; MID_STR_MAX];
             buf[..len].copy_from_slice(s.as_bytes());
             Value::MidStr(Rc::new((len as u8, buf)))

         } else {
             // Strings with length greater than 47
             Value::LongStr(Rc::new(s))
         }
     }
}

Then, the statement at the beginning of this section can be changed to use the into() function:

     self.add_const(var.into());

Generics

So far, the requirements at the beginning of this section have been completed. But since strings can do this, so can other types. And other types of transformations are more intuitive. Listed below are only two conversions from numeric types to the Value type:

impl From<f64> for Value {
    fn from(n: f64) -> Self {
        Value::Float(n)
    }
}

impl From<i64> for Value {
    fn from(n: i64) -> Self {
        Value::Integer(n)
    }
}

Then, adding a numerical type Value to the constant table can also pass the into() function:

     let n = 1234_i64;
     self.add_const(Value::Integer(n)); // old way
     self.add_const(n.into()); // new way

This may seem like a bit of an overkill. But if you implement From for all types that can be converted to Value, then you can put .into() inside add_const():

    fn add_const(&mut self, c: impl Into<Value>) -> usize {
        let c = c.into();

Only the first 2 lines of code of this function are listed here. The following is the original logic of adding constants, which is omitted here.

Look at the second line of code first, put .into() inside the add_const() function, then there is no need for .into() when calling externally. For example, the previous statement of adding strings and integers can be abbreviated as:

     self. add_const(var);
     self. add_const(n);

Many places in the existing code can be modified in this way, and it will become much clearer, so it is worthwhile to implement the From trait for these types.

However, here comes the problem: in the above 2 lines of code, the types of parameters accepted by the two add_const() function calls are inconsistent! In the function definition, how to write this parameter type? The answer lies in the definition of the add_const() function above: c: impl Into<Value>. Its full writing is as follows:

     fn add_const<T: Into<Value>>(&mut self, c: T) -> usize {

This definition means: the parameter type is T, and its constraint is Into<Value>, that is, this T needs to be able to be converted into Value, and no arbitrary type or data structure can be added to the constant table inside.

This is generic in the Rust language! Many books and articles have introduced them very clearly, so we do not introduce generics completely here. In fact, we have used generics very early, such as the definition of the global variable table: HashMap<String, Value>. In most cases, some library defines types and functions with generics, and we just use. And add_const() here is defining a function with generics. The next section will introduce another generic usage example.

Reverse Conversion

The above is to convert the basic type to Value type. But in some cases, the reverse conversion is required, that is, converting the Value type to the corresponding base type. For example, the global variable table of the virtual machine is indexed by the string type, and the name of the global variable is stored in the Value type constant table, so it is necessary to convert the Value type to a string type to be used as an index use. Among them, the read operation and write operation of the global variable table are different, and the corresponding HashMap APIs are as follows:

pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V> // omit the constraint of K, Q
pub fn insert(&mut self, k: K, v: V) -> Option<V>

The difference between reading and writing is that the parameter k of the read get() function is a reference, while the parameter k of the write insert() function is the index itself. The reason is also simple, just use the index when reading, but add the index to the dictionary when writing, and consume k. So we need to realize the conversion of the Value type to the string type itself and its reference, namely String and &String. But for the latter, we use the more generic &str instead. (TODO: should use AsRef here)

impl<'a> From<&'a Value> for &'a str {
     fn from(v: &'a Value) -> Self {
         match v {
             Value::ShortStr(len, buf) => std::str::from_utf8(&buf[..*len as usize]).unwrap(),
             Value::MidStr(s) => std::str::from_utf8(&s.1[..s.0 as usize]).unwrap(),
             Value::LongStr(s) => s,
             _ => panic!("invalid string Value"),
         }
     }
}

impl From<&Value> for String {
     fn from(v: &Value) -> Self {
         match v {
             Value::ShortStr(len, buf) => String::from_utf8_lossy(&buf[..*len as usize]).to_string(),
             Value::MidStr(s) => String::from_utf8_lossy(&s.1[..s.0 as usize]).to_string(),
             Value::LongStr(s) => s.as_ref().clone(),
             _ => panic!("invalid string Value"),
         }
     }
}

The function names of the two conversion calls here are different, std::str::from_utf8() and String::from_utf8_lossy(). The former does not take _lossy and the latter does. The reason lies in UTF-8, etc., which will be explained in detail when UTF8 is introduced later.

In addition, this reverse conversion may fail, such as converting a string Value type to an integer type. But this involves error handling, and we will make modifications after sorting out the error handling in a unified manner. Here still use panic!() to handle possible failures.

After supporting Environment, the global variable table will be re-implemented with Lua table type and Upvalue, then the index will be directly of Value type, and the conversion here is no need any more.

In the code executed by the virtual machine, when reading and writing the global variable table, the conversion of the Value type to a string is completed through into() twice:

                ByteCode::GetGlobal(dst, name) => {
                    let name: &str = (&proto.constants[name as usize]).into();
                    let v = self.globals.get(name).unwrap_or(&Value::Nil).clone();
                    self.set_stack(dst.into(), v);
                }
                ByteCode::SetGlobal(name, src) => {
                    let name = &proto.constants[name as usize];
                    let value = self.stack[src as usize].clone();
                    self.globals.insert(name.into(), value);
                }