goto语句

本节介绍goto语句。

goto语句和label配合,可以进行更加方便的代码控制。但goto语句也有如下限制:

  • 不能跳转到更内层block定义的label,但可以向更外层block跳转;
  • 不能跳转到函数外(注意上面一条规则已经限制了跳转到函数内),我们现在还不支持函数,先忽略这条;
  • 不能跳转进局部变量的作用域,即不能跳过local语句。这里需要注意作用域终止于最后一条非void语句,label被认为是void语句。我个人理解就是不生成字节码的语句。比如下面代码:
  1. while xx do
  2. if yy then goto continue end
  3. local var = 123
  4. -- some code
  5. ::continue::
  6. end

其中的continue label是在局部变量var的后面,但由于是void语句,所以不属于var的作用域,所以上面的goto是合法跳转。

goto语句的实现自然用到Jump字节码即可。语法分析时主要任务就是匹配goto和label,在goto语句的地方生成Jump字节码跳转到对应的label处。由于goto语句可以向前跳转,所以在遇到goto语句时可能还没有遇到对应label的定义;也可以向后跳转,所以遇到label语句时,需要保存起来以便后续的goto匹配。因此需要在ParseProto中新增两个列表,分别保存语法分析时遇到的goto和label信息:

  1. struct GotoLabel {
  2. name: String, // 要跳转到的/定义的label名
  3. icode: usize, // 当前字节码索引
  4. nvar: usize, // 当前局部变量个数,用以判断是否跳转进局部变量作用域
  5. }
  6. pub struct ParseProto<R: Read> {
  7. gotos: Vec<GotoLabel>,
  8. labels: Vec<GotoLabel>,

这两个列表的成员类型一样,都是GotoLabel。其中的nvar是当前的局部变量个数,要确保配对的goto语句对应的nvar不能小于label语句对应的nvar,否则说明在goto和lable语句之间有新的局部变量定义,也就是goto跳转进了局部变量的作用域。

匹配goto语句和lable的实现方式有2种:

  • block结束时一次性匹配:

    • 遇到goto语句,新建GotoLabel加入列表,并生成占位Jump字节码;
    • 遇到label语句,新建GotoLabel加入列表。

    最后在block结束时,一次性匹配,并修复占位字节码。

  • 实时匹配:

    • 遇到goto语句,从现有的label列表中尝试匹配,如果匹配成功则直接生成完整的Jump字节码;否则新建GotoLabel,并生成占位Jump字节码;
    • 遇到label语句,从现有的的goto列表中尝试匹配,如果匹配到则修复对应的占位字节码;由于后续还可能有其他goto语句调整至此,所以仍然需要新建GotoLabel

    分析完毕后就能完成所有的匹配。

可以看到实时匹配虽然略微复杂一点点,但是更内聚,无需最后再执行一个收尾函数。但是这个方案有个大问题:很难判断非void语句。比如本节开头的例子中,解析到continue label时并不能判断后续还有没有其他非void语句。如果有,则是非法跳转。只有解析到block结束才能判断。而在第一个一次性匹配的方案里,是在block结束时做匹配,这个时候就可以方便判断非void语句了。所以,我们这里选择一次性匹配。需要说明的是,在后续介绍Upvalue时会发现一次性匹配方案是有缺陷的。

介绍完上述细节后,语法分析总体流程如下:

  • 进入block后,首先记录下之前(外层)的goto和label的个数;
  • 解析block,记录goto和label语句信息;
  • block结束前,把这个block内出现的goto语句和所有的(包括外层)label语句做匹配:如果有goto语句没匹配到,则仍然放回到goto列表中,因为可能是跳转到block退出后外层定义的label;最后删去block内定义的所有label,因为退出block后就不应该有其他goto语句跳转进来。
  • 在整个Lua chunk结束前,判断goto列表是否为空,如果不为空则说明有的goto语句没有目的地,报错。

对应代码如下:

在解析block开始记录外层已有的goto和label数量;并在block结束之前匹配并清理内部定义的goto和label:

  1. fn block_scope(&mut self) -> Token {
  2. let igoto = self.gotos.len(); // 记录之前外层goto个数
  3. let ilabel = self.labels.len(); // 记录之前外层lable个数
  4. loop {
  5. // 省略其他语句分析
  6. t => { // block结束
  7. self.close_goto_labels(igoto, ilabel); // 退出block前,匹配goto和label
  8. break t;
  9. }
  10. }
  11. }

具体的匹配代码如下:

  1. // 参数igoto和ilable是当前block内定义的goto和label的起始位置
  2. fn close_goto_labels(&mut self, igoto: usize, ilabel: usize) {
  3. // 尝试匹配 “block内定义的goto” 和 “全部label”。
  4. let mut no_dsts = Vec::new();
  5. for goto in self.gotos.drain(igoto..) {
  6. if let Some(label) = self.labels.iter().rev().find(|l|l.name == goto.name) { // 匹配
  7. if label.icode != self.byte_codes.len() && label.nvar > goto.nvar {
  8. // 检查是否跳转进局部变量的作用域。
  9. // 1. label对应的字节码不是最后一条,说明后续有非void语句
  10. // 2. label对应的局部变量数量大于goto的,说明有新定义的局部变量
  11. panic!("goto jump into scope {}", goto.name);
  12. }
  13. let d = (label.icode as isize - goto.icode as isize) as i16;
  14. self.byte_codes[goto.icode] = ByteCode::Jump(d - 1); // 修复字节码
  15. } else {
  16. // 没有匹配上,则放回去
  17. no_dsts.push(goto);
  18. }
  19. }
  20. self.gotos.append(&mut no_dsts);
  21. // 删除函数内部定义的label
  22. self.labels.truncate(ilabel);
  23. }

最后,在chunk解析完毕前,检查所有goto都已经匹配上:

  1. fn chunk(&mut self) {
  2. assert_eq!(self.block(), Token::Eos);
  3. if let Some(goto) = self.gotos.first() {
  4. panic!("goto {} no destination", &goto.name);
  5. }
  6. }

至此完成goto语句。