更多类型
这一节增加简单的类型,包括布尔、整数和浮点数。其他类型比如表和UserData在后续章节中实现。
我们首先完善词法分析以支持这些类型对应的Token,然后语法分析生成对应的字节码,并在虚拟机也增加这些字节码的支持。最后修改函数调用,支持打印这些类型。
完善词法分析
上一章的词法分析只支持2个Token。所以现在无论增加什么特性,都要先改词法分析增加对应的Token。为了避免后面每个章节都零零碎碎地加Token,现在这里一次性加完。
Lua官网列出了完整的词法约定。包括:
name,之前已经实现,用于变量等。
常量,包括字符串、整数和浮点数常量。
关键字:
and break do else elseif end
false for function goto if in
local nil not or repeat return
then true until while
- 符号:
+ - * / % ^ #
& ~ | << >> //
== ~= <= >= < > =
( ) { } [ ] ::
; : , . .. ...
对应的Token定义为:
{{#include ../listing/ch02.variables/src/lex.rs:token}}
具体实现无非繁琐的字符串解析,这里略过。为了简单起见,这次的实现只是支持了大部分简单的类型,而对于复杂类型比如长字符串、长注释、字符串转义、16进制数字暂不支持,浮点数也只不支持科学计数法。这些并不影响后续要增加的主要特性。
值的类型
词法分析支持了更多类型后,接下来在Value中增加这些类型:
{{#include ../listing/ch02.variables/src/value.rs:value}}
其中有点特别的地方是,对于浮点数的输出用了debug模式:{:?}
。因为Rust对浮点数的普通输出格式{}
是整数+小数格式的,而更合理的方式应该是在“整数小数”和“科学计数法”两者中选择更适合的,即C语言printf
里对应的%g
。比如对于数字1e-10
仍然输出"0.000000"
就太不合理了。这似乎是Rust的历史问题。为了兼容等原因,只能使用debug模式{:?}
来对应%g
。这里不深究。
另外,为了便于区分“整数”和“没有小数部分的浮点数”,Lua的官方实现里,对于后者会在后面添加.0
。比如对于浮点数2
会输出为2.0
。如下代码。这个太贴心了。而这也是Rust的{:?}
模式的默认行为,所以不需要我们为此特殊处理。
if (buff[strspn(buff, "-0123456789")] == '\0') { /* looks like an int? */
buff[len++] = lua_getlocaledecpoint();
buff[len++] = '0'; /* adds '.0' to result */
}
在Lua 5.3之前,Lua只有一种数字类型,默认是浮点型。我理解这是因为Lua最初的目的是用于配置文件,面向的是用户而非程序员。对普通用户而言,是不区分整数和浮点数概念的,配置
10秒
和10.0秒
是没有区别的;另外对于一些计算,比如7/2
的结果显而易见是3.5
而不是3
。但随着Lua使用范围的扩大,比如作为很多大型程序间的粘合语言,对整数的需求日益强烈,于是在语言层面区分了整数和浮点数。
语法分析
然后在语法分析中增加这些类型的支持。由于目前只支持函数调用这一种语句,即函数 参数
的格式;而其中“函数”只支持全局变量,所以这次只需要“参数”部分支持这些新类型。Lua语音中的函数调用,参数如果是字符串常量或者表构造,那么就可以省略括号()
,如上一章的”hello, world!”例子。但对于其他情况,比如这次添加的几个新类型,就必须要求有括号()
了。于是对参数部分的修改如下:
Token::Name(name) => {
// function, global variable only
let ic = add_const(&mut constants, Value::String(name));
byte_codes.push(ByteCode::GetGlobal(0, ic as u8));
// argument, (var) or "string"
match lex.next() {
Token::ParL => { // '('
let code = match lex.next() {
Token::Nil => ByteCode::LoadNil(1),
Token::True => ByteCode::LoadBool(1, true),
Token::False => ByteCode::LoadBool(1, false),
Token::Integer(i) =>
if let Ok(ii) = i16::try_from(i) {
ByteCode::LoadInt(1, ii)
} else {
load_const(&mut constants, 1, Value::Integer(i))
}
Token::Float(f) => load_const(&mut constants, 1, Value::Float(f)),
Token::String(s) => load_const(&mut constants, 1, Value::String(s)),
_ => panic!("invalid argument"),
};
byte_codes.push(code);
if lex.next() != Token::ParR { // ')'
panic!("expected `)`");
}
}
Token::String(s) => {
let code = load_const(&mut constants, 1, Value::String(s));
byte_codes.push(code);
}
_ => panic!("expected string"),
}
}
这段代码首先解析函数,跟上一章代码一样,依然只支持全局变量。然后解析参数,除了对字符串常量的支持外,增加了更通用的括号()
的方式。其中处理了各种类型常量:
浮点数常量,跟字符串常量类似,调用
load_const()
函数,在编译时放到常量表里,然后执行时通过LoadConst
字节码来加载。Nil和Boolean类型,就没必要把Nil、true和false也放到常量表里了。直接编码到字节码里更方便,在执行的时候(因为少读一次内存)也更快。所以新增
LoadNil
和LoadBool
字节码。整数常量,则结合了上述两种做法。因为一个字节码4字节,其中opcode占1字节,目的地址占1字节,还剩下2字节,可以存储
i16
的整数。所以对于在i16
范围内的数字(这也是大概率事件),可以直接编码到字节码里,为此新增LoadInt
字节码;如果超过i16
范围,则存在常量表里。这个也是参考的Lua官方实现。由此可以看到Lua对性能的追求,为了减少一次内存访问,而增加一个字节码和代码逻辑。后续也会看到很多这种情况。
由于目前还是只支持函数调用语言,所以执行时函数固定在栈的0
位置,参数固定在1
位置。上述的字节码的目标地址也都固定填的1
。
主要代码介绍完毕。下面再列下用于生成LoadConst
字节码的函数load_const()
定义:
```rust, ignore
fn add_const(constants: &mut Vec
fn load_const(constants: &mut Vec
## 测试
至此,解析过程完成了对新增类型的支持。剩下虚拟机执行部分只是支持新增的几个字节码`LoadInt`、`LoadBool`和`LoadNil`即可。这里略过。
然后就可以测试如下代码:
```lua
{{#include ../listing/ch02.variables/test_lua/types.lua}}
输出结果如下:
[src/parse.rs:64] &constants = [
print,
print,
print,
print,
123456,
print,
123456.0,
]
byte_codes:
GetGlobal(0, 0)
LoadNil(1)
Call(0, 1)
GetGlobal(0, 0)
LoadBool(1, false)
Call(0, 1)
GetGlobal(0, 0)
LoadInt(1, 123)
Call(0, 1)
GetGlobal(0, 0)
LoadConst(1, 1)
Call(0, 1)
GetGlobal(0, 0)
LoadConst(1, 2)
Call(0, 1)
nil
false
123
123456
123456.0
执行正常,但有个小问题,也是上一章就遗留下来的,即print
在常量表里出现了多次。这里需要修改为,在每次添加常量时检查是否已经存在。
添加常量
把上面的add_const()
函数修改如下:
```rust, ignore
fn add_const(constants: &mut Vec
`constants.iter().position()`定位索引。其参数是一个[闭包](https://kaisery.github.io/trpl-zh-cn/ch13-01-closures.html),需要比较两个`Value`,为此需要给`Value`实现`PartialEq` trait:
```rust,ignore
{{#include ../listing/ch02.variables/src/value.rs:peq}}
这里我们认为数值相等的两个整数和浮点数是不同的,比如Integer(123456)
和Float(123456.0)
,因为这确实是两个值,在处理常量表时,不能合并这两个值,否则上一节的测试代码里,最后一行就也是加载整数123456
了。
但在Lua执行过程中,这两个值是相等的,即123 == 123.0
的结果是true
。我们会在后面章节处理这个问题。
回到position()
函数,其返回值是Option<usize>
,Some(i)
代表找到,直接返回索引;而None
代表没有找到,需要先添加常量,再返回索引。按照C语言的编程习惯就是下面的if-else判断,但这里尝试用了更函数化的方式。个人感觉这种方式并没有更加清晰,但既然是在学习Rust,就先尽量优先用Rust的方式。
```rust, ignore if let Some(i) = constants.iter().position(|v| v == &c) { i } else { constants.push(c); constants.len() - 1 }
完成对`add_const()`函数的改造后,常量表里就可以避免出现重复的值。相关输出截取为:
[src/parse.rs:64] &constants = [ print, 123456, 123456.0, ] ```
上述虽然在添加常量时会检查重复,但检查是通过遍历数组做的。添加所有常量的时间复杂度是O(N^2)。如果一个Lua代码段中包含的常量特别多,比如有1百万个,就解析太慢了。为此我们需要一个哈希表来提供快速的查找。TODO。