goto语句

本节介绍goto语句。

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

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

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

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

struct GotoLabel {
    name: String,  // 要跳转到的/定义的label名
    icode: usize,  // 当前字节码索引
    nvar: usize,   // 当前局部变量个数,用以判断是否跳转进局部变量作用域
}

pub struct ParseProto<R: Read> {
    gotos: Vec<GotoLabel>,
    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:

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

具体的匹配代码如下:

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

        // 删除函数内部定义的label
        self.labels.truncate(ilabel);
    }

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

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

至此完成goto语句。