编程语言研究书库

给 SMALL 加功能:赋值、布尔、函数、for 循环,以及一套表驱动测试

给 SMALL 加功能:v2 的语言特性与测试网

这一章干两件事:给 SMALL 装上 v1 缺的所有器官,然后用一张测试网把它们钉死。 新器官清单:赋值、布尔、逻辑运算、函数与闭包、for 循环、转义字符、print/println。 每个器官都落在 v1 架构的某个具体接缝上——你会看到一门语言是怎么「长」出来的, 而不是怎么「重写」出来的。 读完你会拿到「语言特性 ↔ 解释器部件」的对应表,和一套照抄可用的解释器测试模板。

1. 这一章在原书里的位置

原书第 11 章,紧接第 10 章的 v1。v1 证明了流水线跑得通,但只有算术和静态 if 「实用价值有限」1。本章的每一步演进都是一次「需求逼出架构变化」的演示, 这正是它比单独罗列语言特性值钱的地方。

2. 顶层全景:六个新器官,各落在哪个部件上

  新特性                词法器            解析器                求值器
  赋值 x = 5            —               AssignExpression      Env.Update(作用域链)
  (语句→表达式)
  布尔 true/false ! && ||  新 token    新优先级层 + 注册     Boolean 对象 + isTruthy 更新
  函数 func + 调用      —               FuncLiteral/CallExpr  Function 对象 + 新 Env/调用
  闭包                  —               —                     outer 链 + 每次调用新 Env
  for 循环              —               ForStatement          evalForStatement + loopEnv
  字符串转义 \n \t      readString 升级   —                   —
  行列报错              Line/Col 字段     报错拼坐标            —

图说:没有任何一行是「重写」——v1 的 Pratt 骨架、Object 系统、type switch 全部原样保留,新特性只是新表项、新 case、新方法。这是好架构的定义: 长大的时候不用拆掉重来。

3. 语法层的两个关键决策

决策一:赋值从语句变成表达式,而且是语法里优先级最低的那一个。 v1 的 = 只出现在 let 里;想把 i = i + 1 塞进 for 循环的更新位, 赋值必须能出现在「任何表达式在的地方」。新语法的写法2:

  <expression>            ::= <assignment-expression>
  <assignment-expression> ::= <identifier> "=" <assignment-expression>     ← 递归 = 右结合
                            | <logical-or-expression>

「递归定义」带来 a = b = 5 从右向左处理的自然右结合;优先级最低意味着 x = 1 + 2 里加法先算——一个语法位置(优先级)同时解决了两件事。

决策二:逻辑运算插在优先级表的新层。 层级从上(最松)到下(最紧): || → && → ==/!= → < > <= >= → + - → * / → 一元 !/- → 调用 () → 原子。x < 10 && y > 5 不需要括号就是这个分层的直接后果3。

函数与 for 的语法形状(都是 C 风)4:

  <function-declaration> ::= "func" <identifier> "(" [ <identifier> {"," …} ] ")" <block>
  <for-statement>        ::= "for" <let-statement> <expression> ";" <expression> <block>
                           ↑ 初始化必须是 let;更新位放赋值表达式——决策一的回报

4. 词法器的三处手术

① UTF-8 修复(v1 的隐性 bug):Go 对 string 按下标取值拿到的是字节, 源程序里出现多字节字符会把一个字符拆成几个非法字节。修法一行: input: []rune(input)——位置指针从此对齐「逻辑字符」而非内存字节5。

② 吞字符 bug 修复:v1 的 NextToken 末尾无条件 readChar() 前进一格, 但 readString 已经读过了结束引号——字符串后面紧跟 ; 或 ) 时会被跳过。 v2 改成「尊重 helper 留下的光标位置」6。

③ 行列号:Token 增加 Line/Col,词法器在 readChar 里维护;解析器报错时 拼出「第几行第几列」而不是一句含糊的「解析失败」。小功能,大体验7。

5. 求值器:从纯函数到可变状态

Environment 升级成链。 结构体加 outer *Environment 字段; NewEnclosedEnvironment(outer) 造一个指向外层的新环境; 查变量先查本地,没有再沿 outer 递归——作用域链和闭包就靠这一个指针8。

Update():赋值的引擎。 本地有就改本地;没有就递归委托给外层; 到顶还没有,返回 false——x = 10 改一个从未声明的变量会报 「identifier not found for assignment」,赋值不能隐式声明变量9。

case *AssignExpression:
    val := Eval(node.Value, env)               // 先算右边(错误立即传播)
    if _, ok := env.Update(node.Name.Value, val); !ok {
        return newError("identifier not found for assignment: %s", …)
    }
    return val                                  // 赋值本身也是个值(表达式!)

for 循环的执行。 每圈进新 loopEnv(循环变量不污染外层); 体里冒出 ReturnValue 或 Error 直接整个循环退出(return 从循环深处一路 冒到函数顶,靠的就是 v1 那个包装机制)10:

func evalForStatement(fs *ForStatement, env *Environment) Object {
    loopEnv := NewEnclosedEnvironment(env)      // ① 循环专用作用域
    Eval(fs.Init, loopEnv)                       // ② let i = 0(一次)
    for {
        if !isTruthy(Eval(fs.Condition, loopEnv)) { break }  // ③ 条件(每圈)
        res := Eval(fs.Body, loopEnv)            // ④ 体(每圈)
        if res != nil && (res.Type() == RETURN_VALUE_OBJ || res.Type() == ERROR_OBJ) {
            return res                           //    return/错误 → 整个循环退出
        }
        Eval(fs.Update, loopEnv)                 // ⑤ i = i + 1(每圈)
    }
    return NULL
}

函数与闭包:func 定义产生 Function 对象(存形参 + 函数体 + 定义时的环境); 调用时开一个新 Env、外层就是定义时的环境(所以内层函数能记住外层变量——闭包), 实参绑定形参后执行函数体。每次调用一个新 Env,递归才不会撞变量11。

6. 主线例子:三个程序,三个器官

书上用三个程序验证新器官,每个都是「器官 + 具体输出」的对照:

布尔(booleans.usm):isDay && isRaining、!isRaining || isDay、 x < y 直接打印 true/false——比较终于返回真布尔,v1 的 1/0 退役12。

递归(factorial.usm):

  func factorial(x) {
      if (x == 0) { return 1; } else { return x * factorial(x - 1); }
  }
  let result = factorial(10);
  → Factorial of 10 is: 3628800

10 层递归 = 10 个嵌套的 Environment,每层 x 各是各的值; 3628800 这个数同时证明调用栈管理和算术都对13。

循环(loops.usm):for let i = 0; i < 5; i = i + 1 { sum = sum + i; } 逐圈打印 Index: 2 -> Sum so far: 3,最后 Final Total: 10—— 初始化一次、条件每圈、体每圈、更新每圈,四拍一个不多14。

7. 测试网:七组表驱动测试

本章的另一半同样承重:测试不是附录,是「语言语义的定义书」。 七组测试,每组锁一类语义15:

测试钉死的语义代表用例
TestEvalIntegerExpression优先级与分组5 + 2 * 10 → 25;2 * (5 + 10) → 30;(5 + 10*2 + 15/3)*2 + -10 → 50
TestEvalBooleanExpression真布尔与比较(1 < 2) == true → true;true != false → true
TestIfElseExpressions分支与 truthinessif (1) {10} → 10(整数 1 为真);无 else 且假 → NULL
TestReturnStatements急停与死代码return 10; 9; → 10(后面的 9 永远执行不到);嵌套 if 里的 return → 10
TestLetStatements绑定与链式引用let a = 5; let b = a; let c = a + b + 5; c; → 15
TestAssignmentsUpdate 语义let a = 5; a = a * 2; a = a + 5; a; → 15
TestForLoops四拍循环 + 嵌套 + 提前退出双重循环 2×2 → 4;循环里 i==5 时 return → 5
TestClosuresAndScoping闭包捕获newAdder(2) 返回的函数记住 x=2,addTwo(2) → 4

图说:最后一组是最见功力的一格:let newAdder = func(x) { func(y) { x + y }; }; 内层函数引用外层参数 x;外层返回后 x 本该「死」了,但闭包通过 outer 链 让 x 活着——addTwo(2) = 4 证明作用域链的查找是「定义时」的而不是 「调用时」的。这是解释器课程里最容易讲错、也最能分清「懂没懂」的一个测试。

判断(我们的,不是书里的): 这组测试的真正价值是「语义即测试」—— 每行用例都是语言规范的一句话。后人在循环里加 break,只需加一行用例; 改优先级常量,前四组测试会立刻造反。没有这张网,语言演进就是裸奔—— 这也是为什么测试放在功能章而不是单独附录。 如果错,会错在: 表驱动测试覆盖的是「输入 → 输出」的正向语义; 错误路径(类型错误、参数个数不匹配)的用例在本章偏少, 「语义定义书」对失败语义的刻画还不够全。

8. 作者的判断与证据

判断性质依据
赋值必须是表达式,for 循环才可能架构推断(因果清楚)「syntactically impossible」原句2
优先级用 iota + map 两张表管理技术选择(可扩展性证据)新运算符只动表不动逻辑3
更新未声明变量报错而非隐式声明语言语义的设计立场Update 返回 false 即错9
每次函数调用新 Environment正确性必需(递归不撞名)阶乘递归的实测13
测试即规范作者隐式立场(测试置于功能章)七组测试与语义的一一对应15

9. 边界与局限

  • 函数是一等值但没名字绑定细节之外的检查:参数个数不匹配的行为 本章没写测试;调用非函数值的错误路径未演示。
  • 没有 break/continue:循环提前退出只能靠 return 出整个函数—— 累加测试里「i==5 就 return」就是这种限制的绕法。
  • 闭包的 Update 语义有微妙处:内层函数改外层变量(Update 沿链上溯)允许, 但按值捕获还是引用捕获的边界没讨论——newAdder 测试只验了读,没验改。
  • 性能一概未测:解释器的 Eval 递归、Environment 链的查找成本, 在深度递归(阶乘 10 层无恙,1 万层呢?)下没有数据。
  • 行列号只进了解析器错误;运行时的错误(如 assignment not found) 不带坐标——报错体验只完整了一半。

10. 可带走的

  1. 语言演进 = 在既有架构上加部件:新 token(词法器)、新优先级+注册(解析器)、 新 case/新对象(求值器)——好架构的试金石就是「长大的成本」。
  2. 赋值表达式化是 for 循环的前置条件;右结合用「递归语法规则」表达。
  3. 优先级一张表(iota 常量)+ 一张 map(token → 层级)= Pratt 的全部弹药。
  4. 作用域链 = Environment 的 outer 指针;闭包 = 函数记着定义时的环境。
  5. Update 沿链递归;给未声明变量赋值是错误,不是隐式声明。
  6. for 四拍:初始化一次、条件每圈、体每圈、更新每圈;体里 return/错误直接掀桌。
  7. 测试即规范:优先级、truthiness、急停、闭包,每格语义一行用例钉死。
  8. 闭包测试 newAdder(2)(2) = 4 验的是「定义时作用域」—— 解释器懂没懂作用域,就看这一格。

11. 原文地图

主题原书章原文位置
为什么扩展语言Why do we need to extendtext/112-ch11.txt:23(搜「enhances its utility」)
v2 新特性清单与 EBNFNew features in SMALLtext/113-fm-new-features-in-small.txt:8(搜「assignments」) · :72(搜「assignment-expression」) · :90(搜「logical-or-expression」) · :49(搜「function-declaration」)
词法器 []rune 与吞字符修复Implementing the updated versiontext/114-fm-implementing-the-updated-version-of-small.txt:27(搜「[]rune」) · :35(搜「synchronization issue」)
优先级常量与 mapImplementing the updated versiontext/114-fm-implementing-the-updated-version-of-small.txt:76(搜「iota」) · :92(搜「precedences = map」)
行列号报错Implementing the updated versiontext/114-fm-implementing-the-updated-version-of-small.txt:124(搜「Line and Col」)
parseForStatementImplementing the updated versiontext/114-fm-implementing-the-updated-version-of-small.txt:134(搜「parseForStatement」)
Update 与 AssignExpressionImplementing the updated versiontext/114-fm-implementing-the-updated-version-of-small.txt:184(搜「func (e *Environment) Update」) · :209(搜「AssignExpression」)
evalForStatement 与闭包机制Implementing the updated versiontext/114-fm-implementing-the-updated-version-of-small.txt:231(搜「evalForStatement」) · :266(搜「NewEnclosedEnvironment」)
booleans/factorial/loops 实测Implementing the updated versiontext/114-fm-implementing-the-updated-version-of-small.txt:293(搜「booleans.usm」) · :346(搜「factorial.usm」) · :359(搜「3628800」) · :370(搜「loops.usm」)
七组测试Writing tests for SMALLtext/115-fm-writing-tests-for-small.txt:10(搜「TestEvalIntegerExpression」) · :193(搜「TestAssignments」) · :218(搜「TestForLoops」) · :296(搜「TestClosuresAndScoping」)

Footnotes

  1. 出处:「Adding New Features to SMALL」第 1–10 段(text/112-ch11.txt:8,搜「restricted to simple arithmetic」)。 ↩

  2. 出处:「New features in SMALL」(text/113-fm-new-features-in-small.txt:72,搜「assignment-expression」)。「made constructs such as C-style for loops…syntactically impossible」的实现侧解释见 text/114-fm-implementing-the-updated-version-of-small.txt:48(搜「fully-fledged expression」)。 ↩ ↩2

  3. 出处:「New features in SMALL」(text/113-fm-new-features-in-small.txt:90,搜「logical-or-expression」;:98,搜「bind more tightly」)。iota 常量表见 text/114-fm-implementing-the-updated-version-of-small.txt:76(搜「iota」)。 ↩ ↩2

  4. 出处:「New features in SMALL」(text/113-fm-new-features-in-small.txt:46,搜「if-statement」;:47,搜「for-statement」;:49,搜「function-declaration」;:149,搜「escape-sequence」)。 ↩

  5. 出处:「Implementing the updated version of SMALL」(text/114-fm-implementing-the-updated-version-of-small.txt:30,搜「[]rune(input)」;:21,搜「returns a byte, not a character」)。 ↩

  6. 出处:「Implementing the updated version of SMALL」第 35–43 段(text/114-fm-implementing-the-updated-version-of-small.txt:35,搜「synchronization issue」)。 ↩

  7. 出处:「Implementing the updated version of SMALL」第 122–131 段(text/114-fm-implementing-the-updated-version-of-small.txt:124,搜「Line and Col」)。 ↩

  8. 出处:「Implementing the updated version of SMALL」第 262–269 段(text/114-fm-implementing-the-updated-version-of-small.txt:266,搜「NewEnclosedEnvironment」)。「first checking the immediate local scope and then recursively searching up the chain」。 ↩

  9. 出处:「Implementing the updated version of SMALL」(text/114-fm-implementing-the-updated-version-of-small.txt:184,搜「func (e *Environment) Update」;:214,搜「identifier not found for assignment」)。 ↩ ↩2

  10. 出处:「Implementing the updated version of SMALL」(text/114-fm-implementing-the-updated-version-of-small.txt:231,搜「evalForStatement」;:249,搜「RETURN_VALUE_OBJ」)。 ↩

  11. 出处:「Implementing the updated version of SMALL」(text/114-fm-implementing-the-updated-version-of-small.txt:365,搜「creating a fresh, isolated Environment」)。 ↩

  12. 出处:「Implementing the updated version of SMALL」(text/114-fm-implementing-the-updated-version-of-small.txt:293,搜「booleans.usm」;:335,搜「10 < 20 is: true」)。 ↩

  13. 出处:「Implementing the updated version of SMALL」(text/114-fm-implementing-the-updated-version-of-small.txt:346,搜「factorial.usm」;:359,搜「3628800」)。 ↩ ↩2

  14. 出处:「Implementing the updated version of SMALL」(text/114-fm-implementing-the-updated-version-of-small.txt:370,搜「loops.usm」;:388,搜「Sum so far: 0」;:394,搜「Final Total: 10」)。 ↩

  15. 出处:「Writing tests for SMALL」(text/115-fm-writing-tests-for-small.txt:10,搜「TestEvalIntegerExpression」;:51,搜「TestEvalBooleanExpression」;:90,搜「TestIfElseExpressions」;:128,搜「TestReturnStatements」;:167,搜「TestLetStatements」;:193,搜「TestAssignments」;:218,搜「TestForLoops」;:296,搜「TestClosuresAndScoping」)。 ↩ ↩2

On this page