while和break语句

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

while语句

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

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

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

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

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

  1. pub enum ByteCode {
  2. Jump(i16),

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

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

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

break语句

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

  1. while 123 do -- 外层循环block,支持break
  2. while true do -- 中层循环block,支持break
  3. a = a + 1
  4. if a < 10 then -- 内层block,不支持break
  5. break -- 跳出 `while true do` 的循环
  6. end
  7. end
  8. 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字段,类型是“跳转字节码索引列表”的列表:

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

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

  1. fn while_stat(&mut self) {
  2. // 省略条件判断语句处理部分
  3. // 调用block()前,追加一个列表
  4. self.break_blocks.push(Vec::new());
  5. // 调用block()
  6. assert_eq!(self.block(), Token::End);
  7. // 调用block()后,弹出刚才追加的列表,并修复其中的跳转字节码
  8. for i in self.break_blocks.pop().unwrap().into_iter() {
  9. self.byte_codes[i] = ByteCode::Jump((iend - i) as i16);
  10. }
  11. }

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

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

continue语句?

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