Value and Type

The previous section defined the bytecode, and mentioned at the end that we need two tables, the constant table and the global variable table, respectively to maintain the relationship between constants/variables and "values", so their definitions depend on the "Value" 's definition in Lua. This section introduces and defines Lua's value.

For the convenience of description, all the words "variable" in this section later include variables and constants.

Lua is a dynamically typed language, and the "type" is bound to a value, not to a variable. For example, in the first line of the following code, the variable n contains the information: "the name is n"; while value 10 contains the information: "the type is an integer" and "the value is 10". So in line 2, it's OK to assign n to a different type value.

local n = 10
n = "hello" -- OK

For comparison, here's the statically typed language Rust. In the first line, the information of n is: "the name is n" and "the type is i32"; the information of 10 is: "the value is 10". It can be seen that the "type" information has changed from the attribute of the variable to the attribute of the value. So you can't assign n to a string value later.

let mut n: i32 = 10;
n = "hello"; // !!! Wrong

The following two diagrams represent the relationship between variables, values, and types in dynamically typed and statically typed languages, respectively:

 variable         values                   variable               values
+---------+      +---------------+        +---------------+      +-----------+
| name: n |--\-->| type: Integer |        | name: n       |----->| value: 10 |
+---------+  |   | value: 10     |        | type: Integer |  |   +-----------+
             |   +---------------+        +---------------+  X
             |                                               |
             |   +----------------+                          |   +----------------+
             \-->| type: String   |                          \-->| value: "hello" |
                 | value: "hello" |                              +----------------+
                 +----------------+

          dynamic type                              static type
    "type" is bound to values                 "type" is bound to variables

Value

In summary, the value of Lua contains type information. This is also very suitable for defining with enum:

use std::fmt;
use crate::vm::ExeState;

#[derive(Clone)]
pub enum Value {
    Nil,
    String(String),
    Function(fn (&mut ExeState) -> i32),
}

impl fmt::Debug for Value {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        match self {
            Value::Nil => write!(f, "nil"),
            Value::String(s) => write!(f, "{s}"),
            Value::Function(_) => write!(f, "function"),
        }
    }
}

Currently 3 types are defined:

  • Nil, Lua's null value.
  • String for the hello, world! string. For the associated value type, the simplest String is temporarily used, and it will be optimized later.
  • Function for print. The associated function type definition refers to the C API function definition typedef int (*lua_CFunction) (lua_State *L); in Lua, and will be improved later. Among them, ExeState corresponds to lua_State, which will be introduced in the next section.

Other types such as integers, floating-point numbers and tables will be added in the future.

Above the Value definition, the Clone trait is implemented via #[derive(Clone)]. This is because Value will definitely involve assignment operations, and the String type includes Rust's string String, which does not support direct copying, namely the Copy trait is not implemented, or it owns the data on the heap. So we can only declare the whole Value as Clone. All assignments involving Value need to be done through clone(). It seems that the performance is worse than direct assignment. We will discuss this issue later when we define more types.

We also manually implemented the Debug trait to define the print format, after all, the function of the current object code is to print "hello, world!". Since the function pointer parameter associated with Function does not support the Debug trait, it cannot be automatically implemented by #[derive(Debug)].

Two Tables

After defining the Value, we can define the two tables mentioned at the end of the previous section.

Constant table stores constants. Bytecodes refer to constants by index directly, so constant tables can be represented by Rust's variable-length array Vec<Value>.

The global variable table, which stores global variables according to their names, can temporarily be represented by Rust's HashMap<String, Value>. We will change this later.

Compared with the ancient C language, components such as Vec and HashMap in the Rust standard library have brought great convenience and consistent experience.