TT Lab
开始
学习 学习路径 课程

编译器 — 从头到尾亲手打造一门小语言

随身带着声明时的环境 — 闭包

在 TT Lab 中继续学习

一句话总结

树遍历解释器(tree-walking interpreter)为 AST 的每种节点写一个函数,规定“遇到这种节点就计算什么”。名字在环境(environment,一条链:字典里是名字到值的映射,并指向外层环境)中查找,而函数值会携带声明时的环境一起走。这就是闭包。

为什么需要它

第 6 个模块的字节码 VM 和第 8 个模块的机器码虽然快,但离“定义含义”太远。“&& 的左边为假就不计算右边”“块结束后内层的名字就消失”“函数看到的是它被声明之处的变量”,这样的规则,最直截了当的写法就在树解释器里。所以本课程把解释器当作基准。后面的 VM 和原生代码,运行同一个程序时,必须输出与解释器相同的行才算正确。

不过,“直截了当地写”并不等于“容易”。用 Python 写解释器,Python 的含义会悄悄混进来。

如果定义含义的一方漏掉了这些,后面所有的实现都会按错误的基准来对齐。

工作原理

环境是一条链。每进入一个块、每调用一次函数,就创建一个新环境,并让它指向父环境。查找名字时沿着链向上走,而赋值则修改这个名字所在环境里的那一格——不会在当前环境里新建一个。

fn counter(start) {           counter(10) 을 부르면
  let n = start;                [호출 환경 {start:10, n:10}] → [전역 {counter, …}]
  fn next() { n = n + 1;        next 를 선언하는 순간, 함수 값 = (next 의 선언, 지금 환경)
              return n; }
  return next;                 counter 가 끝나도 호출 환경은 next 가 붙잡고 있어 살아남는다
}
let a = counter(10);           a() → 새 환경 {} → 부모는 붙잡아 둔 호출 환경 → n 을 11 로
print a(); print a();          11, 12 — 같은 칸을 바꾸기 때문에 이어진다

关键在于,函数值的父环境,是声明时的环境,而不是调用时的环境(静态作用域)。如果把调用时的环境作为父环境(动态作用域),a() 就找不到 counter 的 n,而且如果全局里有同名的名字,就会莫名其妙地改掉那个全局的。闭包把环境复制走同样不对——两个抓住同一个环境的函数(一个把值加一,一个读取值)就会看到不同的格子。

return 一次就要跳出好几层。while 里的 if 里的 return,必须直接回到调用的位置。在 Python 中,最简单的办法是抛出一个异常(ReturnSignal),在调用位置捕获。调用结束后,必须把调用深度恢复,而在靠异常跳出的路径上很容易漏掉这一步(这正是需要 finally 的地方)。

运行时错误标在运算符的位置。在 a / b 中,如果 b 是 0,就以 / 的行:列报出 “runtime error: division by zero”。语义分析不知道而放过的类型(作为参数传进来的 true),也在这里被抓住。同时存活的调用超过 200 个,就以 “stack overflow” 停止——如果先碰到了 Python 自己的递归上限,用户看到的就会是 Python 的错误。

在现场相遇的样子

下一项实验要做什么

在 interp.py 中做出值规则(64 位回绕、向 0 截断的除法、区分 bool 和 int)、环境链、表达式求值(短路求值与运行时类型检查)、语句与块、if 和 while、函数与闭包、return 和调用深度,以及从解析一直连到执行的 run_source。评分器会把几百对随机数、固定程序,以及 150 个随机程序打印出的行和运行时错误,与参考对照。