block和goto的逃逸
上一节介绍了Upvalue的从函数中的逃逸。但是实际上局部变量的作用域是block,所以每当block结束时就可能出现Upvalue的逃逸。而函数也可以看成是一种block,所以上一节介绍的从函数中逃逸可以看成是从block中逃逸的一种特殊情况。
另外,还有一种逃逸的场景,即goto语句向后跳转,跳过局部变量的定义,此时局部变量也会失效。
上一节因为内容太多了,为了不节外生枝,所以把这两个逃逸的场景放到这一节中单独介绍。
从block中逃逸
首先看个从block中逃逸的示例代码:
do
local i = 0
c1 = function ()
i = i + 1 -- upvalue
print(i)
end
end -- block结束,局部变量i失效
这个例子中,do .. end
block中定义的匿名函数引用了其中定义的局部变量i
,作为Upvalue。当block结束后,局部变量i
就会失效,但由于还被匿名函数引用,所以需要逃逸。
虽然函数可以看成是block的一种特殊情况,但特殊情况毕竟是特殊情况,处理更通用的从block中的逃逸还是很不一样。上一节中当函数结束时,在Return/Return0/TailCall
等相关字节码中关闭所有Upvalue,因为每个函数的结尾都会有这几个字节码之一。但是block结束并没有类似固定的字节码,所以为此新增一个字节码Close
,这个字节码关闭当前block中被Upvalue引用的局部变量。
最简单的做法是在每个block结尾处都生成一个Close
字节码,但是由于从block中逃逸的情况非常少见,为了这种很少见的情况而对所有block都增加一个字节码,实在是得不偿失。所以,需要在语法分析阶段判断这个block中是否有逃逸,如果没有,则无需生成Close
字节码。
接下来就是如何判断一个block中是否有局部变量逃逸的现象。可能有多种实现方法。比如参考上节中多层函数嵌套的方式,也维护一个block嵌套的关系。不过有一种更轻量的做法,就是对每个局部变量加上一个标记位,如果被Upvalue引用,则设置这个标记位。然后在block结束的时候,判断这个block中定义的局部变量有没有被标记过,就能知道是否需要生成Close
字节码。
这里省略Close
字节码的具体定义和执行流程。
从goto中逃逸
我实在想不出一个合理的从goto中逃逸的示例。但是不合理示例的还是可以构造一个出来:
::again::
if c1 then -- 第1次执行到这里时if为假
c1() -- 下面给c1赋值后,c1就是包括了一个Upvalue的闭包
end
local i = 0
c1 = function ()
i = i + 1 -- upvalue
print(i)
end
goto again
上述代码中,第1次执行时if判断为假,跳过对c1
的调用;下面给c1赋值后,c1就是包括了一个Upvalue的闭包;然后goto跳转回开头后,此时就可以调用c1了;但是此时局部变量i
也已经失效,所以需要关闭。
可以把上述代码中,从开头的again
label定义到最后的goto
语句也看成是一个block,那么就可以采用刚才介绍的从block中逃逸的做法,来处理goto语句了。但是goto语句有个特殊的地方。我们之前在介绍goto语句的时候介绍过,对label和goto语句的匹配,有两种做法:
- 边解析边匹配。即解析到label的时候就匹配已经出现过的goto语句;解析到goto语句时就匹配已经出现过的label;
- block结束后(也就是其中定义的label失效时),一次性匹配现有的label和goto语句。
这两个做法的实现难度差不多,但是由于goto语句的另外一个特征,即向前跳转的goto语句需要忽略void语句。之前为了更方便的处理void语句,就采用了上述第2个方案。但是,现在为了支持逃逸,在解析到goto语句的时候(准确说是在生成的Jump
字节码之前),可能会生成一个Close
字节码。具体会不会生成,取决于goto向后跳转时,是否跳过了逃逸的局部变量的定义。也就是说,只有匹配label和goto语句才能知道是否需要Close
字节码。如果还按照第2个方案在block结束后再做匹配的话,在block结束时即便发现需要生成Close
也无法再插入到字节码序列中了。所以,就只能改成上述第1个边解析边匹配的方案了,在匹配的时候及时判断是否需要生成Close
字节码。