值和类型
上一节定义了字节码,并且在最后提到我们还需要两个表,常量表和全局变量表,分别维护常量和变量跟“值”之间的关系,所以其定义就依赖Lua值的定义。本节就介绍并定义Lua的值。
为方便叙述,本节中后续所有“变量”一词包括变量和常量。
Lua是动态类型语言,“类型”是跟值绑定,而不是跟变量绑定。比如下面代码第一行,等号前面变量n包含的信息是:“名字是n”;等号后面包含的信息是:“类型是整数”和“值是10”。所以在第2行还是可以把n赋值为字符串的值。
local n = 10
n = "hello" -- OK
作为对比,下面是静态类型语言Rust。第一行,等号前面包含的信息是:“名字是n” 和 “类型是i32”;等号后面的信息是:“值是10”。可以看到“类型”信息从变量的属性变成了值的属性。所以后续就不能把n赋值为字符串的值。
let mut n: i32 = 10;
n = "hello"; // !!! Wrong
下面两个图分别表示动态类型和静态类型语言中,变量、值和类型之间的关系:
变量 值 变量 值
+--------+ +----------+ +----------+ +----------+
| 名称:n |--\------>| 类型:整数 | | 名称:n |-------->| 值: 10 |
+--------+ | | 值: 10 | | 类型:整数 | | +---------+
| +----------+ +----------+ X
| |
| +------------+ | +------------+
\------>| 类型:字符串 | \--->| 值:"hello" |
| 值:"hello" | +------------+
+------------+
动态类型 静态类型
类型跟值绑定 类型跟变量绑定
值Value
综上,Lua的值是包含了类型信息的。这也非常适合用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"),
}
}
}
当前定义了3种类型:
Nil
,Lua的空值。String
,用于hello, world!
字符串。关联值类型暂时使用最简单的String,后续会做优化。Function
,用于print
。关联的函数类型定义是参考Lua中的C API函数定义typedef int (*lua_CFunction) (lua_State *L);
,后续会做改进。其中ExeState
对应lua_State
,在下一节介绍。
可以预见后续还会增加整数、浮点数、表等类型。
在Value定义的上面,通过#[derive(Clone)]
实现了Clone
trait。这是因为Value肯定会涉及到赋值操作,而我们现在定义的String类型包含了Rust的字符串String
,后者是不支持直接拷贝的,即没有实现Copy
trait,或者说其拥有堆heap上的数据。所以只能把整个Value也声明为Clone
的。后续所有涉及Value的赋值,都需要通过 clone()
来实现。看上去比直接赋值的性能要差一些。我们后续在定义了更多类型后,还会讨论这个问题。
我们还手动实现了Debug
trait,定义打印格式,毕竟当前目标代码的功能就是打印"hello, world!"。由于其中的Function
关联的函数指针参数不支持Debug
trait,所以不能用#[derive(Debug)]
的方式来自动实现。
两个表
定义好值Value后,就可以定义上一节最后提到的两个表了。
常量表,用来存储所有需要的常量。字节码直接用索引来引用常量,所以常量表可以用Rust的可变长数组Vec<Value>
表示。
全局变量表,根据变量名称保存全局变量,可以暂时用Rust的HashMap<String, Value>
表示。
相对于古老的C语言,Rust标准库里
Vec
和HashMap
这些组件带来了很大的方便。不用自己造轮子,并提供一致的体验。