这一章实现了函数。
函数是用 `LoxCallable` interface 表示的。Native 函数,直接用 java 代码实现这个 LoxCallable interface 即可。Lox 代码定义的函数,在 parse 之后创建为 LoxFunction 实例(见 visitFunctionStmt)。
`return` 语句通过 Java 的异常实现。`return` 也是一种控制流语句,它从任意地方返跳回到函数调用边界。借助 Java 异常来实现,是非常省事的办法。否则,就需要自己显式操作控制流了,麻烦程度大增。Lox 不支持 continue 和 break,如果要支持的话,也可使用同样的办法。
另外发现,实现 `return` 时,没有检查 `return` 语句是否处于函数定义中。在解释器中可以直接输入 `return;`,它会正常执行,并因抛出的 Java 异常( `Return`)未被捕获而导致解释器退出。正常来说,应当在 semantic analysis 阶段检查出来并报错的。且看后续章节会不会涉及这些。(剧透:下一章就会解决。)
本章的精髓在于,如何使用 Environment 管理作用域。
Environment 是在第 8 章 Statements and State 为了支持语句块(bloc statement)而引入的。在这一章,它成为了正确实现函数的关键因素之一。现代语言几乎都支持作用域(scope),支持作用域嵌套,名字的 shadowing 等等机制。函数方面,往往还支持高阶函数和闭包。
Interpreter 类里面有 globals 和 environment 两个类型为 Environment 的字段。值得注意。
- globals 表示全局 Environment。
- Interpreter.environment 表示 Interpreter 的当前 environment。
刚开始运行时,它们是同一个对象。表示 Interpreter 处于全局作用域。
随着 Interpreter 进入/退出新的作用域,Interpeter.environment 会随之变化。
进入新的作用域时(executeBlock),会以当前 environment 创建新的 Environment,并以之取代 Interpreter 的当前 environment。
退出作用域时,Interpreter.environment 会恢复为之前的 Environment。
定义变量(visitVarStmt)和函数(visitFunctionStmt)时,均是将名字定义在 Interpreter.environment 即当前 Environment 里面。
这意味着,如果我们在全局定义变量和函数,那么它们就是全局作用域可见的;如果在某个局部作用域里定义变量和函数,它们就是局部作用域可见的。
```lox
{
var x = 1;
fun localFn() {
print "localFn";
}
localFn();
}
// Error: Undefined variable 'localFn'.
localFn();
```
比如,上面这段代码中,`localFn` 只在代码块里面可见。
我们还可以在函数内部定义函数,它们只在函数内部可见。
```lox
fun fn() {
var x = 1;
fun inner() {
print "inner";
}
inner();
}
// Error: Undefined variable 'inner'.
inner();
```
实现函数调用(`LoxFunction.call` 方法),也需要仔细考虑如何与 Environment 交互。执行函数调用需要在单独的 Environment 中进行,函数参数和局部变量,都需要在这个 Environment 中定义。但是,函数还需要引用其他名字,比如其他函数、全局变量等。
在这一章的初版实现中,函数调用的 Environment 的 enclosing Environment 为 Interpreter 的全局 Environment(`Interpreter.globals`)。这样,在函数内部就可以正确引用在全局作用域里定义的函数和变量了。但是,这样做是不够的。
它会导致一个问题,在语句块和函数内部定义的函数,无法正确引用语句块里和外层函数里的局部变量。比如上面两个示例中,`localFn` 和 `inner` 函数,都无法引用它外层的 `x` 变量。
看到这里也许会想,函数调用时,一定要用 `globals` Environment 吗?用解释器的当前 Environment (即 `Interpreter.environment`)应该能解决问题?答案仍然是否定的。它使得我们能够引用外层的名字,却破坏了「静态作用域」规则,使用得作用域变成动态的了:函数中名字的含义将由调用位置决定,而不是由函数在源码中的定义位置决定。
```lox
var lang = "Lox";
fun printLang{
print lang;
}
{
var lang = "Java";
printLang();
}
```
定义 `printLang` 时,根据它在源码中的位置,函数体里的 `lang` 会解析到全局作用域中的 `lang` 绑定。以后无论从哪里调用 `printLang`,它都不会改为访问调用者的局部 `lang`。
而如果我们修改实现的话,在语句块中调用 `printLang()` 时,将会打印出 Java。这就违背静态作用域的规则了。
这里的困境,与闭包有关。以下用这个经典例子来说明:
```lox
fun makeCounter() {
var i = 0;
fun count() {
i = i + 1;
print i;
}
return count;
}
var counter = makeCounter();
counter(); // "1".
counter(); // "2".
```
按照初版实现,在调用 `makeCounter` 时,会创建一个 Environment,并将 `i` 就是定义到这个 Environment 中。但调用完毕,这个 Environment 直接被丢弃,`i` 的绑定也无法查找了。
解决办法也简单,我们不要丢弃这个 Environment,而是要保存到内部嵌套函数 `count` 对应的 `LoxFunction` 对象里面。这个 Environment 称为 closure。而且函数调用时,也使用 closure 作为 enclosing Environment。这样,函数就可以使用外层作用域里定义的变量了(而不是使用 `Interpreter.globals` 作为 enclosing Environment)。
在执行语句块时,也会创建一个 Environment。对于在语句块中定义的函数,也是照此办理,逻辑是相同的。
而这个 closure Environment 最终仍然会引用全局 Environment,所以在全局作用域中定义的函数、变量等,也可正常访问。
至此,Lox 对于函数、作用域、闭包的支持,已经相当不错了。但是,closure 的实现仍有一些问题,这就留待下一章揭晓了。
quoting看完了 Crafting Interpreters 第 9 章,Control Flow。
nevent1q…wxfd
这一章增加了逻辑表达式(and、or),if 语句,while 和 for 循环。
实现 for 循环时,介绍了一下语法糖 syntactic sugar,在 parse 时,就把 for 循环给改写为 while 循环语句了。这就是脱糖 desugaring 过程。
Lox 的循环不支持 continue 和 break 语句。所以大大降低了实现难度。
而因为同样原因,我感觉语法分析阶段做的事情非常少。基本上 parse 得到语法树(Stmt class)之后,就结束了,没有额外工作,感觉有所缺憾。希望后续章节能够涉及这方面。
nevent1q…5d8d
