第 9 章:控制流(Control Flow)
原文:Robert Nystrom, Crafting Interpreters, Chapter 9。原书以 CC BY-NC-SA 4.0 协议发布;本译文用于学习与研究。
图灵机能做的任何事,其他能做到同样事情的机器也能做到。
Alan Turing
变量让程序有状态,控制流让程序能根据状态选择路径、重复执行。加入条件、逻辑运算与循环后,Lox 成为图灵完备语言:从计算能力的角度,它能表达任何可计算过程。
9.1 图灵机(简述)(Turing Machines, Briefly)
图灵机是一种极简计算模型:无限纸带保存符号,读写头读取或修改当前格,有限状态机依据状态和符号移动并改变状态。其看似简单,却能模拟任何通用计算机。

Lox 不像图灵机,但只要能保存任意多状态、在条件下选择不同操作、并能重复操作,就具备同等计算表达力。变量提供状态,if 提供条件分支,循环提供重复。因此本章的功能并非普通便利,而是跨越了重要理论门槛。
9.2 条件执行(Conditional Execution)
if 的文法与 AST:
statement -> "if" "(" expression ")" statement
( "else" statement )?
| ... ;
defineAst(outputDir, "Stmt", Arrays.asList(
// 既有节点省略。
"If : Expr condition, Stmt thenBranch, Stmt elseBranch"
));
解析 if:
private Stmt ifStatement() {
consume(LEFT_PAREN, "Expect '(' after 'if'.");
Expr condition = expression();
consume(RIGHT_PAREN, "Expect ')' after if condition.");
Stmt thenBranch = statement();
Stmt elseBranch = null;
if (match(ELSE)) {
elseBranch = statement();
}
return new Stmt.If(condition, thenBranch, elseBranch);
}
在 statement() 开头加入:
if (match(IF)) return ifStatement();
解释器根据 Lox 的真值规则执行其中一个分支:
@Override
public Void visitIfStmt(Stmt.If stmt) {
if (isTruthy(evaluate(stmt.condition))) {
execute(stmt.thenBranch);
} else if (stmt.elseBranch != null) {
execute(stmt.elseBranch);
}
return null;
}
悬挂 else 是经典语法问题:else 总与最近的、尚未匹配 else 的 if 结合。递归下降中,内层 ifStatement() 会先消费它,因此自然得到这一通行规则。

9.3 逻辑运算符(Logical Operators)
第 3 章定义了 and 与 or,现在实现其短路语义。AST 新增:
"Logical : Expr left, Token operator, Expr right"
文法在赋值与相等比较之间加入两个层次:
expression -> assignment ;
assignment -> IDENTIFIER "=" assignment | logic_or ;
logic_or -> logic_and ( "or" logic_and )* ;
logic_and -> equality ( "and" equality )* ;
解析函数与二元表达式相似:
private Expr or() {
Expr expr = and();
while (match(OR)) {
Token operator = previous();
Expr right = and();
expr = new Expr.Logical(expr, operator, right);
}
return expr;
}
private Expr and() {
Expr expr = equality();
while (match(AND)) {
Token operator = previous();
Expr right = equality();
expr = new Expr.Logical(expr, operator, right);
}
return expr;
}
assignment() 的基础规则改为 or()。解释时不可先计算两个操作数,否则会失去短路:
@Override
public Object visitLogicalExpr(Expr.Logical expr) {
Object left = evaluate(expr.left);
if (expr.operator.type == OR) {
if (isTruthy(left)) return left;
} else {
if (!isTruthy(left)) return left;
}
return evaluate(expr.right);
}
or 的左值为真时立即返回左值,and 的左值为假时立即返回左值;否则才计算右侧。注意返回的不是强制转换后的布尔值,而是原始操作数:"yes" or "no" 的结果是 "yes"。这种行为方便表达默认值和条件选择。
9.4 while 循环(While Loops)
while 的文法、AST 与解析:
statement -> "while" "(" expression ")" statement | ... ;
"While : Expr condition, Stmt body"
private Stmt whileStatement() {
consume(LEFT_PAREN, "Expect '(' after 'while'.");
Expr condition = expression();
consume(RIGHT_PAREN, "Expect ')' after condition.");
Stmt body = statement();
return new Stmt.While(condition, body);
}
解释器每轮重新计算条件:
@Override
public Void visitWhileStmt(Stmt.While stmt) {
while (isTruthy(evaluate(stmt.condition))) {
execute(stmt.body);
}
return null;
}
例如:
var a = 1;
while (a < 10) {
print a;
a = a + 1;
}
9.5 for 循环(For Loops)
Lox 的 for 语法很熟悉:
for (var i = 0; i < 10; i = i + 1) {
print i;
}
完整文法:
forStmt -> "for" "(" ( varDecl | exprStmt | ";" )
expression? ";"
expression? ")" statement ;
for 的初始化、条件、增量均可缺省。解析器不需要为它生成专用 AST 节点,而是把它脱糖(desugar)为已有节点:
private Stmt forStatement() {
consume(LEFT_PAREN, "Expect '(' after 'for'.");
Stmt initializer;
if (match(SEMICOLON)) {
initializer = null;
} else if (match(VAR)) {
initializer = varDeclaration();
} else {
initializer = expressionStatement();
}
Expr condition = null;
if (!check(SEMICOLON)) {
condition = expression();
}
consume(SEMICOLON, "Expect ';' after loop condition.");
Expr increment = null;
if (!check(RIGHT_PAREN)) {
increment = expression();
}
consume(RIGHT_PAREN, "Expect ')' after for clauses.");
Stmt body = statement();
if (increment != null) {
body = new Stmt.Block(Arrays.asList(
body,
new Stmt.Expression(increment)));
}
if (condition == null) condition = new Expr.Literal(true);
body = new Stmt.While(condition, body);
if (initializer != null) {
body = new Stmt.Block(Arrays.asList(initializer, body));
}
return body;
}
一个 for:
for (initializer; condition; increment) body;
最终等价于:
{
initializer;
while (condition) {
body;
increment;
}
}
条件缺省时变为 true,形成无限循环。用块包住初始化器还能确保循环变量只在该 for 的作用域内可见。
9.5.1 脱糖(Desugaring)
语法糖是让程序员写得更方便、但可翻译为已有更基础构造的语法。for 只是 while、块和表达式语句的表层写法,因此没有必要让解释器专门实现 visitForStmt()。

脱糖将复杂性放在解析器,令解释器保持小而正交。它也是语言实现中常见的策略:许多表面特性都可在某个早期阶段归约为较小的核心语言。但脱糖必须谨慎保持语义,例如本例中增量必须在每次循环主体后执行,初始化器的作用域也必须正确。
挑战(Challenges)
- 添加
break语句。它应如何穿过嵌套语句到达最近循环?异常、标志或解释器返回值各有什么优缺点? - 添加
continue。对于脱糖后的for,如何确保continue仍会执行增量表达式? - 添加
do { ... } while (condition);,并比较其主体至少执行一次的语义与while。 - 实现三元条件运算符
condition ? then : otherwise,并确定优先级、结合性、求值顺序和短路行为。
设计笔记:一勺勺语法糖(Spoonfuls of Syntactic Sugar)
语法糖不是“无用的花哨”。合适的糖能让常见意图更短、更清楚,降低样板代码与出错机会;for 正是把初始化、测试和更新放进读者习惯的紧凑结构中。
但每个新表面构造也有成本:要学习、实现、测试、记录,并与错误消息、工具和未来特性互动。可脱糖不等于免费,脱糖后的核心语义也可能暴露边界差异。语言设计者要问:该特性是否解决真实、常见的问题?是否和既有语言模型一致?能否以很小的实现复杂度获得显著可读性收益?
健康的语言通常不是完全无糖,也不是无限堆叠甜味。小而正交的核心加上少量高价值的表面便利,往往比庞杂的特殊规则更持久。