给 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: 362880010 层递归 = 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 | 分支与 truthiness | if (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 |
| TestAssignments | Update 语义 | 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. 可带走的
- 语言演进 = 在既有架构上加部件:新 token(词法器)、新优先级+注册(解析器)、 新 case/新对象(求值器)——好架构的试金石就是「长大的成本」。
- 赋值表达式化是 for 循环的前置条件;右结合用「递归语法规则」表达。
- 优先级一张表(iota 常量)+ 一张 map(token → 层级)= Pratt 的全部弹药。
- 作用域链 = Environment 的 outer 指针;闭包 = 函数记着定义时的环境。
- Update 沿链递归;给未声明变量赋值是错误,不是隐式声明。
- for 四拍:初始化一次、条件每圈、体每圈、更新每圈;体里 return/错误直接掀桌。
- 测试即规范:优先级、truthiness、急停、闭包,每格语义一行用例钉死。
- 闭包测试
newAdder(2)(2) = 4验的是「定义时作用域」—— 解释器懂没懂作用域,就看这一格。
11. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 为什么扩展语言 | Why do we need to extend | text/112-ch11.txt:23(搜「enhances its utility」) |
| v2 新特性清单与 EBNF | New features in SMALL | text/113-fm-new-features-in-small.txt:8(搜「assignments」) · :72(搜「assignment-expression」) · :90(搜「logical-or-expression」) · :49(搜「function-declaration」) |
| 词法器 []rune 与吞字符修复 | Implementing the updated version | text/114-fm-implementing-the-updated-version-of-small.txt:27(搜「[]rune」) · :35(搜「synchronization issue」) |
| 优先级常量与 map | Implementing the updated version | text/114-fm-implementing-the-updated-version-of-small.txt:76(搜「iota」) · :92(搜「precedences = map」) |
| 行列号报错 | Implementing the updated version | text/114-fm-implementing-the-updated-version-of-small.txt:124(搜「Line and Col」) |
| parseForStatement | Implementing the updated version | text/114-fm-implementing-the-updated-version-of-small.txt:134(搜「parseForStatement」) |
| Update 与 AssignExpression | Implementing the updated version | text/114-fm-implementing-the-updated-version-of-small.txt:184(搜「func (e *Environment) Update」) · :209(搜「AssignExpression」) |
| evalForStatement 与闭包机制 | Implementing the updated version | text/114-fm-implementing-the-updated-version-of-small.txt:231(搜「evalForStatement」) · :266(搜「NewEnclosedEnvironment」) |
| booleans/factorial/loops 实测 | Implementing the updated version | text/114-fm-implementing-the-updated-version-of-small.txt:293(搜「booleans.usm」) · :346(搜「factorial.usm」) · :359(搜「3628800」) · :370(搜「loops.usm」) |
| 七组测试 | Writing tests for SMALL | text/115-fm-writing-tests-for-small.txt:10(搜「TestEvalIntegerExpression」) · :193(搜「TestAssignments」) · :218(搜「TestForLoops」) · :296(搜「TestClosuresAndScoping」) |
Footnotes
-
出处:「Adding New Features to SMALL」第 1–10 段(
text/112-ch11.txt:8,搜「restricted to simple arithmetic」)。 ↩ -
出处:「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 -
出处:「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 -
出处:「New features in SMALL」(
text/113-fm-new-features-in-small.txt:46,搜「if-statement」;:47,搜「for-statement」;:49,搜「function-declaration」;:149,搜「escape-sequence」)。 ↩ -
出处:「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」)。 ↩ -
出处:「Implementing the updated version of SMALL」第 35–43 段(
text/114-fm-implementing-the-updated-version-of-small.txt:35,搜「synchronization issue」)。 ↩ -
出处:「Implementing the updated version of SMALL」第 122–131 段(
text/114-fm-implementing-the-updated-version-of-small.txt:124,搜「Line and Col」)。 ↩ -
出处:「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」。 ↩ -
出处:「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 -
出处:「Implementing the updated version of SMALL」(
text/114-fm-implementing-the-updated-version-of-small.txt:231,搜「evalForStatement」;:249,搜「RETURN_VALUE_OBJ」)。 ↩ -
出处:「Implementing the updated version of SMALL」(
text/114-fm-implementing-the-updated-version-of-small.txt:365,搜「creating a fresh, isolated Environment」)。 ↩ -
出处:「Implementing the updated version of SMALL」(
text/114-fm-implementing-the-updated-version-of-small.txt:293,搜「booleans.usm」;:335,搜「10 < 20 is: true」)。 ↩ -
出处:「Implementing the updated version of SMALL」(
text/114-fm-implementing-the-updated-version-of-small.txt:346,搜「factorial.usm」;:359,搜「3628800」)。 ↩ ↩2 -
出处:「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」)。 ↩ -
出处:「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