值和类型

上一节定义了字节码,并且在最后提到我们还需要两个表,常量表和全局变量表,分别维护常量和变量跟“值”之间的关系,所以其定义就依赖Lua值的定义。本节就介绍并定义Lua的值。

为方便叙述,本节中后续所有“变量”一词包括变量和常量。

Lua是动态类型语言,“类型”是跟值绑定,而不是跟变量绑定。比如下面代码第一行,等号前面变量n包含的信息是:“名字是n”;等号后面包含的信息是:“类型是整数”和“值是10”。所以在第2行还是可以把n赋值为字符串的值。

  1. local n = 10
  2. n = "hello" -- OK

作为对比,下面是静态类型语言Rust。第一行,等号前面包含的信息是:“名字是n” 和 “类型是i32”;等号后面的信息是:“值是10”。可以看到“类型”信息从变量的属性变成了值的属性。所以后续就不能把n赋值为字符串的值。

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

下面两个图分别表示动态类型和静态类型语言中,变量、值和类型之间的关系:

  1. 变量 变量
  2. +--------+ +----------+ +----------+ +----------+
  3. | 名称:n |--\------>| 类型:整数 | | 名称:n |-------->| 值: 10 |
  4. +--------+ | | 值: 10 | | 类型:整数 | | +---------+
  5. | +----------+ +----------+ X
  6. | |
  7. | +------------+ | +------------+
  8. \------>| 类型:字符串 | \--->| 值:"hello" |
  9. | 值:"hello" | +------------+
  10. +------------+
  11. 动态类型 静态类型
  12. 类型跟值绑定 类型跟变量绑定

值Value

综上,Lua的值是包含了类型信息的。这也非常适合用enum来定义:

  1. {{#include ../listing/ch01.hello_world/src/value.rs}}

当前定义了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标准库里VecHashMap这些组件带来了很大的方便。不用自己造轮子,并提供一致的体验。