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也已经失效,所以需要关闭。

可以把上述代码中,从开头的againlabel定义到最后的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字节码。