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语句,新建
-
实时匹配:
- 遇到goto语句,从现有的label列表中尝试匹配,如果匹配成功则直接生成完整的Jump字节码;否则新建
GotoLabel
,并生成占位Jump字节码; - 遇到label语句,从现有的的goto列表中尝试匹配,如果匹配到则修复对应的占位字节码;由于后续还可能有其他goto语句调整至此,所以仍然需要新建
GotoLabel
。
分析完毕后就能完成所有的匹配。
- 遇到goto语句,从现有的label列表中尝试匹配,如果匹配成功则直接生成完整的Jump字节码;否则新建
可以看到实时匹配虽然略微复杂一点点,但是更内聚,无需最后再执行一个收尾函数。但是这个方案有个大问题:很难判断非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语句。