if语句

条件判断语句跟之前已经实现的语句最大的不同是:不再顺序执行字节码了,可能出现跳转。为此,我们新增字节码Test,关联2个参数:

  • 第一个参数,u8类型,判断条件在栈上的位置;
  • 第二个参数,u16类型,向前跳转字节码条数。

这个字节码的语义是:如果第一个参数代表的语句为假,那么向前跳转第二个参数指定条数的字节码。控制结构图如下:

  1. +-------------------+
  2. | if condition then |---\ 如果condition为假,则跳过block
  3. +-------------------+ |
  4. |
  5. block |
  6. |
  7. +-----+ |
  8. | end | |
  9. +-----+ |
  10. <-----------------------/

Test字节码的定义如下:

  1. pub enum ByteCode {
  2. // condition structures
  3. Test(u8, u16),

第二个参数是跳转的字节码条数,即相对位置。如果使用绝对位置,那么解析和执行的代码都会稍微简单一些,但表达能力就会差一些。16bit的范围是65536。如果使用绝对位置,那么一个函数内超过65536之后的代码,就不能使用跳转字节码了。而如果使用相对位置,那么就支持在字节码本身的65536范围内跳转,就可以支持很长的函数了。所以我们使用相对位置。这也引入了一个一直忽略的问题,就是字节码中参数的范围,比如栈索引参数都是用的u8类型,那么如果一个函数中超过了256个局部变量,就会溢出,造成bug。后续需要特别处理参数的范围问题。

按照上面的控制结构图,完成if语句的语法分析代码如下:

  1. fn if_stat(&mut self) {
  2. let icond = self.exp_discharge_top(); // 读取判断语句
  3. self.lex.expect(Token::Then); // 语法:then关键字
  4. self.byte_codes.push(ByteCode::Test(0, 0)); // 生成Test字节码占位,两个参数后续补上
  5. let itest = self.byte_codes.len() - 1;
  6. // 解析语法块!并预期返回end关键字,暂时不支持elseif和else分支
  7. assert_eq!(self.block(), Token::End);
  8. // 修复Test字节码参数。
  9. // iend为字节码序列当前位置,itest为Test字节码位置,两者差就是需要跳转的字节码条数。
  10. let iend = self.byte_codes.len() - 1;
  11. self.byte_codes[itest] = ByteCode::Test(icond as u8, (iend - itest) as u16);
  12. }

代码流程已经在注释里逐行说明。这里需要详细说明的是递归调用的block()函数。

block的结束

原来的block()函数实际是整个语法分析的入口,只执行一次(而没有递归调用),一直读到源代码末尾Token::Eos作为结束:

  1. fn block(&mut self) {
  2. loop {
  3. match self.lex.next() {
  4. // 这里省略其他语句解析
  5. Token::Eos => break, // Eos则退出
  6. }
  7. }
  8. }

现在要支持的if语句中的代码块,预期的结束是关键字end;后续还会包括elseifelse等其他关键字。代码块的结束并不只是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,作为函数返回值,返回给调用者。我们采用这种方法,代码如下:

  1. fn block(&mut self) -> Token {
  2. loop {
  3. match self.lex.next() {
  4. // 这里省略其他语句解析
  5. t => break t, // 返回t
  6. }
  7. }
  8. }

所以上面的if_stat()函数中,就要判断block()的返回值为Token::End

  1. // 解析语法块!并预期返回end关键字,暂时不支持elseif和else分支
  2. assert_eq!(self.block(), Token::End);

而原来的语法分析入口函数chunk()也要增加对block()返回值的判断:

  1. fn chunk(&mut self) {
  2. assert_eq!(self.block(), Token::Eos);
  3. }

block的变量作用域

block()函数另外一个需要改动的地方是局部变量的作用域。即在block内部定义的局部变量,在外部是不可见的。

这个功能很核心!不过实现却非常简单。只要在block()入口处记录当前局部变量的个数,然后在退出前清除新增的局部变量即可。代码如下:

  1. fn block(&mut self) -> Token {
  2. let nvar = self.locals.len(); // 记录原始的局部变量个数
  3. loop {
  4. match self.lex.next() {
  5. // 这里省略其他语句解析
  6. t => {
  7. self.locals.truncate(nvar); // 失效block内部定义的局部变量
  8. break t;
  9. }
  10. }
  11. }
  12. }

在后续介绍了Upvalue后,还需要做其他处理。

do语句

上面两小节处理了block的问题。而最简单的创建block的语句是do语句。因为太简单了,就在这里顺便介绍。语法分析代码如下:

  1. // BNF:
  2. // do block end
  3. fn do_stat(&mut self) {
  4. assert_eq!(self.block(), Token::End);
  5. }

虚拟机执行

之前的虚拟机执行是顺序依次执行字节码,用Rust的for语句循环遍历即可:

  1. pub fn execute<R: Read>(&mut self, proto: &ParseProto<R>) {
  2. for code in proto.byte_codes.iter() {
  3. match *code {
  4. // 这里省略所有字节码的预定义逻辑
  5. }
  6. }
  7. }

现在要支持Test字节码的跳转,就需要在循环遍历字节码序列期间,能够修改下一次遍历的位置。Rust的for语句不支持在循环过程中修改遍历位置,所以需要手动控制循环:

  1. pub fn execute<R: Read>(&mut self, proto: &ParseProto<R>) {
  2. let mut pc = 0; // 字节码索引
  3. while pc < proto.byte_codes.len() {
  4. match proto.byte_codes[pc] {
  5. // 这里省略其他字节码的预定义逻辑
  6. // condition structures
  7. ByteCode::Test(icond, jmp) => {
  8. let cond = &self.stack[icond as usize];
  9. if matches!(cond, Value::Nil | Value::Boolean(false)) {
  10. pc += jmp as usize; // jump if false
  11. }
  12. }
  13. }
  14. pc += 1; // 下一条字节码
  15. }
  16. }

通过字节码位置pc来控制循环执行。所有字节码执行后pc自增1,指向下一个字节码;对于跳转字节码Test则额外会修改pc。由于Test字节码最后也会执行pc自增,所以其跳转的位置其实是目标地址减去1。其实可以在这里加一条continue;语句,跳过最后面的pc自增。不知道这两种做法哪个更好。

上述代码的判断中可以看到,Lua语言中的假值只有2个:nil和false。其他值比如0,空表等,都是真值。

测试

至此我们实现了最简单的if条件判断语句。

由于我们目前还不支持关系运算,所以if后面的判断条件只能用其他语句。测试代码如下:

  1. {{#include ../listing/ch06.control_structures/test_lua/if.lua}}

第一个判断语句中的条件语句a是未定义全局变量,值为nil,是假,所以内部的语句不执行。

第二个判断语句中的条件语句print是已经定义的全局变量,是真,所以内部语句会执行。block内部定义了局部变量a,在内部正常执行,但在block结束后,a就无效了,再引用就是作为未定义全局变量了,打印就是nil