while和break语句

本节介绍while语句,并引入break语句。

while语句

跟if语句的简单形式(不包括elseif和else分支)相比,while语句只是在内部block的末尾增加一个无条件跳转字节码,跳转回语句开头。如下图中左边的跳转所示:

/--->+----------------------+
|    | while condition then |---\ 如果condition为假,则跳过block
|    +----------------------+   |
|                               |
|        block                  |
\<----                          |
     +-----+                    |
     | end |                    |
     +-----+                    |
     <--------------------------/

最终生成的字节码序列的格式如下,其中...代表内部代码块的字节码序列:

/-->  Test --\  if分支
|     ...    |
\---  Jump   |
        <----/  整个while语句结尾

语法分析过程和代码,也是在if语句的基础上增加一个无条件跳转字节码,这里略过。需要改造的一个地方是,这里的无条件跳转是向后跳转。之前Jump字节码的第2个参数是u16类型,只能向前跳转。现在需要改为i16类型,用负数表示向后跳转:

pub enum ByteCode {
    Jump(i16),

相应的,虚拟机执行部分需要修改为:

        // 无条件跳转
        ByteCode::Jump(jmp) => {
            pc = (pc as isize + jmp as isize) as usize;
        }

相对于C语言,Rust的类型管理更加严格,所以看起来比较啰嗦。

break语句

while语句本身非常简单,但引入了另外一个语句:break。break语句本身也很简单,无条件跳转到block结尾处即可,但问题在于并不是所有block都支持break,比如之前介绍的if内部的block就不支持break,只有循环语句的block支持break。准确说,break要跳出的是最近一层的循环的block。比如下面示例:

while 123 do  -- 外层循环block,支持break
    while true do  -- 中层循环block,支持break
        a = a + 1
        if a < 10 then  -- 内层block,不支持break
            break  -- 跳出 `while true do` 的循环
        end
    end
end

代码中有3层block,外层和中层的while的block支持break,内层if的block不支持break。此时break就是跳出中层的block。

如果break语句不处于循环block之内,则语法错误。

为实现上述功能,可以在block()函数中增加一个参数,以便在递归调用时表示最近一次的循环block。由于在生成跳转字节码时block还未结束,还不知道跳转目的地址,所以只能先生成跳转字节码占位,而参数留空;后续在block结束的位置再修复字节码参数。所以block()函数的参数就是最近一层循环block的break跳转字节码索引列表。调用block()函数时,

  • 如果是循环block,则创建一个新索引列表作为调用参数,在调用结束后,用当前地址(即block结束位置)修复列表中字节码;
  • 如果不是循环block,则使用当前列表(也就是当前最近循环block)作为调用参数。

但是block()函数的递归调用并不是直接递归,而是间接递归。如果要这样传递参数,那么所有的语法分析函数都要加上这个参数了,太复杂。所以把这个索引列表放到全局的ParseProto中。牺牲了局部性,换来了编码方便。

下面来看具体编码实现。首先在ParseProto中添加break_blocks字段,类型是“跳转字节码索引列表”的列表:

pub struct ParseProto<R: Read> {
    break_blocks: Vec::<Vec::<usize>>,

在解析while语句时,调用block()函数前,追加一个列表;调用后,修复列表中的跳转字节码:

    fn while_stat(&mut self) {

        // 省略条件判断语句处理部分

        // 调用block()前,追加一个列表
        self.break_blocks.push(Vec::new());

        // 调用block()
        assert_eq!(self.block(), Token::End);

        // 调用block()后,弹出刚才追加的列表,并修复其中的跳转字节码
        for i in self.break_blocks.pop().unwrap().into_iter() {
            self.byte_codes[i] = ByteCode::Jump((iend - i) as i16);
        }
    }

在准备好block后,就可以实现break语句了:

    fn break_stat(&mut self) {
        // 取最近的循环block的字节码列表
        if let Some(breaks) = self.break_blocks.last_mut() {
            // 生成跳转字节码占位,参数留空
            self.byte_codes.push(ByteCode::Jump(0));
            // 追加到字节码列表中
            breaks.push(self.byte_codes.len() - 1);
        } else {
            // 如果没有循环block,则语法错误
            panic!("break outside loop");
        }
    }

continue语句?

实现了break语句后,自然就想到continue语句。而且continue的实现跟break类似,区别就是一个跳转到循环结束,一个跳转到循环开头,加上这个功能就是顺手的事情。不过Lua不支持continue语句!其中有一小部分原因跟repeat..until语句有关。我们在下一节介绍完repeat..until语句后再详细讨论continue语句。