关于 scope [下集]

Enochii published at 2020-12-16

上篇文章 关于 scope 结尾的时候,留了个坑。说支持支持闭包的静态作用域(无语病)不是很难,然而事实证明是我 naive 了,还是需要花点功夫的。

这里先给出我原来的想法,然后指出哪里错了,最后给出正确的实现思路。

错误的思路

我原来的想法是把函数定义和函数定义点的 environment 进行绑定,看起来像这样:

image

问题其实和我们 scope 的表示有点关系,考虑如下代码:

var a = "global";
{
    fun f() {
        println a;
    }
    f(); // global
    var a = "local";
    f(); // local
}

我们预期的结果是两个 f() 的调用都打印 “global” ,然而事与愿违。以下分别为两次调用时对应的 environment chain :

image

第二次 f() 调用对名字 a 的解析,在经过 Block Environment 时就已经被截获了,这就是万恶之源。

问题出在我们希望函数定义绑定的是一个定义点环境的 snapshot(快照),而我们的 Environment 代表的是一个运行时的 scope,在新定义一个变量后就会被篡改。也就是说,对 f() 的多次调用看到的 Environment 可能是不一样的。

正确的思路

解决这个问题一般有两个思路:

思路一:一个定义一个 Environment

现在我们的 Environment 类定义大概长这样:

class Environment {
    Map<String, Object> bindings; // bindings in this scope
    Environment enclosing; // outside environment
    ...
}

这种做法一个 Environment 实例会对应一个 scope (或者一对大括号),在对应大括号内新发生一个定义后,这个 Environment 的 bindings 就会发生改变。

思路一是把这种粗粒度的 Environment 拆分成多个小的环境,也就是我们说的一个定义会创建一个 Environment ,据此我们可以有:

class Environment {
    Pair<String, Object> bindings; // the only binding in this environment
    Environment enclosing; // outside environment
    ...
}

对于下列代码

{
    // [1]
    var a = "a";
    // [2]
    var b = "b";
    // [3]
    var c = "c";
    // [4]
}

忽略 block 的外层环境,对于 4 个点,我们的 Environment 分别如下:

image

简单来说,[1] 处看不到任何 binding,[2] 可以看到 a ,[3] 可以看到 a b,[4] 可以看到 a b c

这就完成了我们想要的『快照』。如果你写过 Lisp 或它的方言,你应该会发现它的链表表达这个逻辑简直是浑然天成。在定义一个新名字时,我们可以:

(define new-env (cons <new-binding> old-env)

对于快照,我们只要 hold old-env 这个指针,后续定义新的环境会创建新的链表,old-env 不会受到影响。在退出一个 scope 时,reset 环境指针就好了!

思路二:词法地址

思路二是使用词法地址,我们很容易有这样的想法:既然作用域是静态的,那么为什么不在运行前就把 use-definition relation(指名字的使用和定义)确定下来呢?即对于程序中出现的每一个对变量的使用,我们都可以找到声明(定义)它的 scope。

据此,我们可以在运行前,将名字的使用 resolve 到某一个特定的 Environment 。这样一来在运行时直接到这个 Environment 搜索该名字即可。

词法地址除了解决 scope 的问题,还优化了名字的查找。在名字查找时,我们不用沿着 environment chain 从里到外屁颠跑一整躺了。

具体的实现可以在 parsing 后对语法树再做一次语义分析,具体可以参考这里,或者后续有时间我补一篇……

另外,如果 Environment 类的 bindingsArray 而不是 Map ,我们可以一步到位,这样词法地址就是一个 pair ,它由两部分组成:

总结

本文把上篇博客留下的关于静态作用域的坑补上了,简单介绍了玩具解释器中静态作用域的两种实现方式。跑步去啦!go go go!