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 thehello, world!
string. For the associated value type, the simplestString
is temporarily used, and it will be optimized later.Function
forprint
. The associated function type definition refers to the C API function definitiontypedef int (*lua_CFunction) (lua_State *L);
in Lua, and will be improved later. Among them,ExeState
corresponds tolua_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
andHashMap
in the Rust standard library have brought great convenience and consistent experience.