值和类型

上一节定义了字节码,并且在最后提到我们还需要两个表,常量表和全局变量表,分别维护常量和变量跟“值”之间的关系,所以其定义就依赖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标准库里VecHashMap这些组件带来了很大的方便。不用自己造轮子,并提供一致的体验。