if语句
条件判断语句跟之前已经实现的语句最大的不同是:不再顺序执行字节码了,可能出现跳转。为此,我们新增字节码Test
,关联2个参数:
- 第一个参数,
u8
类型,判断条件在栈上的位置; - 第二个参数,
u16
类型,向前跳转字节码条数。
这个字节码的语义是:如果第一个参数代表的语句为假,那么向前跳转第二个参数指定条数的字节码。控制结构图如下:
+-------------------+
| if condition then |---\ 如果condition为假,则跳过block
+-------------------+ |
|
block |
|
+-----+ |
| end | |
+-----+ |
<-----------------------/
Test字节码的定义如下:
pub enum ByteCode {
// condition structures
Test(u8, u16),
第二个参数是跳转的字节码条数,即相对位置。如果使用绝对位置,那么解析和执行的代码都会稍微简单一些,但表达能力就会差一些。16bit的范围是65536。如果使用绝对位置,那么一个函数内超过65536之后的代码,就不能使用跳转字节码了。而如果使用相对位置,那么就支持在字节码本身的65536范围内跳转,就可以支持很长的函数了。所以我们使用相对位置。这也引入了一个一直忽略的问题,就是字节码中参数的范围,比如栈索引参数都是用的u8
类型,那么如果一个函数中超过了256个局部变量,就会溢出,造成bug。后续需要特别处理参数的范围问题。
按照上面的控制结构图,完成if语句的语法分析代码如下:
fn if_stat(&mut self) {
let icond = self.exp_discharge_top(); // 读取判断语句
self.lex.expect(Token::Then); // 语法:then关键字
self.byte_codes.push(ByteCode::Test(0, 0)); // 生成Test字节码占位,两个参数后续补上
let itest = self.byte_codes.len() - 1;
// 解析语法块!并预期返回end关键字,暂时不支持elseif和else分支
assert_eq!(self.block(), Token::End);
// 修复Test字节码参数。
// iend为字节码序列当前位置,itest为Test字节码位置,两者差就是需要跳转的字节码条数。
let iend = self.byte_codes.len() - 1;
self.byte_codes[itest] = ByteCode::Test(icond as u8, (iend - itest) as u16);
}
代码流程已经在注释里逐行说明。这里需要详细说明的是递归调用的block()
函数。
block的结束
原来的block()
函数实际是整个语法分析的入口,只执行一次(而没有递归调用),一直读到源代码末尾Token::Eos
作为结束:
fn block(&mut self) {
loop {
match self.lex.next() {
// 这里省略其他语句解析
Token::Eos => break, // Eos则退出
}
}
}
现在要支持的if语句中的代码块,预期的结束是关键字end
;后续还会包括elseif
和else
等其他关键字。代码块的结束并不只是Token::Eos
了,就需要修改block()
函数,把不是合法语句开头的Token(比如Eos
,关键字end
等)都认为是block的结束,并交由调用者判断是否为预期的结束。具体编码有2种修改方法:
-
用
lex.peek()
代替上述代码中的lex.next()
,如果看到的Token不是合法语句开头,则退出循环,此时这个Token还没有被消费读取。外部调用者再调用lex.next()
读取这个Token做判断。如果这么做,那么目前所有的语句处理代码,都要在最开始加上一个lex.next()
来跳过看到的Token,比较啰嗦。比如上一段里的if_stat()
函数,就要用lex.next()
跳过关键字if
。 -
仍然用
lex.next()
,对于读到的不是合法语句开头的Token,作为函数返回值,返回给调用者。我们采用这种方法,代码如下:
fn block(&mut self) -> Token {
loop {
match self.lex.next() {
// 这里省略其他语句解析
t => break t, // 返回t
}
}
}
所以上面的if_stat()
函数中,就要判断block()
的返回值为Token::End
:
// 解析语法块!并预期返回end关键字,暂时不支持elseif和else分支
assert_eq!(self.block(), Token::End);
而原来的语法分析入口函数chunk()
也要增加对block()
返回值的判断:
fn chunk(&mut self) {
assert_eq!(self.block(), Token::Eos);
}
block的变量作用域
block()
函数另外一个需要改动的地方是局部变量的作用域。即在block内部定义的局部变量,在外部是不可见的。
这个功能很核心!不过实现却非常简单。只要在block()
入口处记录当前局部变量的个数,然后在退出前清除新增的局部变量即可。代码如下:
fn block(&mut self) -> Token {
let nvar = self.locals.len(); // 记录原始的局部变量个数
loop {
match self.lex.next() {
// 这里省略其他语句解析
t => {
self.locals.truncate(nvar); // 失效block内部定义的局部变量
break t;
}
}
}
}
在后续介绍了Upvalue后,还需要做其他处理。
do语句
上面两小节处理了block的问题。而最简单的创建block的语句是do语句。因为太简单了,就在这里顺便介绍。语法分析代码如下:
// BNF:
// do block end
fn do_stat(&mut self) {
assert_eq!(self.block(), Token::End);
}
虚拟机执行
之前的虚拟机执行是顺序依次执行字节码,用Rust的for语句循环遍历即可:
pub fn execute<R: Read>(&mut self, proto: &ParseProto<R>) {
for code in proto.byte_codes.iter() {
match *code {
// 这里省略所有字节码的预定义逻辑
}
}
}
现在要支持Test
字节码的跳转,就需要在循环遍历字节码序列期间,能够修改下一次遍历的位置。Rust的for语句不支持在循环过程中修改遍历位置,所以需要手动控制循环:
pub fn execute<R: Read>(&mut self, proto: &ParseProto<R>) {
let mut pc = 0; // 字节码索引
while pc < proto.byte_codes.len() {
match proto.byte_codes[pc] {
// 这里省略其他字节码的预定义逻辑
// condition structures
ByteCode::Test(icond, jmp) => {
let cond = &self.stack[icond as usize];
if matches!(cond, Value::Nil | Value::Boolean(false)) {
pc += jmp as usize; // jump if false
}
}
}
pc += 1; // 下一条字节码
}
}
通过字节码位置pc
来控制循环执行。所有字节码执行后pc
自增1,指向下一个字节码;对于跳转字节码Test
则额外会修改pc
。由于Test
字节码最后也会执行pc
自增,所以其跳转的位置其实是目标地址减去1。其实可以在这里加一条continue;
语句,跳过最后面的pc
自增。不知道这两种做法哪个更好。
上述代码的判断中可以看到,Lua语言中的假值只有2个:nil和false。其他值比如0,空表等,都是真值。
测试
至此我们实现了最简单的if条件判断语句。
由于我们目前还不支持关系运算,所以if后面的判断条件只能用其他语句。测试代码如下:
if a then
print "skip this"
end
if print then
local a = "I am true"
print(a)
end
print (a) -- should be nil
第一个判断语句中的条件语句a
是未定义全局变量,值为nil
,是假,所以内部的语句不执行。
第二个判断语句中的条件语句print
是已经定义的全局变量,是真,所以内部语句会执行。block内部定义了局部变量a
,在内部正常执行,但在block结束后,a
就无效了,再引用就是作为未定义全局变量了,打印就是nil
。