Skip to main content

第 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 总与最近的、尚未匹配 elseif 结合。递归下降中,内层 ifStatement() 会先消费它,因此自然得到这一通行规则。

原书插图:else 可对应的两种解释。

9.3 逻辑运算符(Logical Operators)

第 3 章定义了 andor,现在实现其短路语义。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)

  1. 添加 break 语句。它应如何穿过嵌套语句到达最近循环?异常、标志或解释器返回值各有什么优缺点?
  2. 添加 continue。对于脱糖后的 for,如何确保 continue 仍会执行增量表达式?
  3. 添加 do { ... } while (condition);,并比较其主体至少执行一次的语义与 while
  4. 实现三元条件运算符 condition ? then : otherwise,并确定优先级、结合性、求值顺序和短路行为。

设计笔记:一勺勺语法糖(Spoonfuls of Syntactic Sugar)

语法糖不是“无用的花哨”。合适的糖能让常见意图更短、更清楚,降低样板代码与出错机会;for 正是把初始化、测试和更新放进读者习惯的紧凑结构中。

但每个新表面构造也有成本:要学习、实现、测试、记录,并与错误消息、工具和未来特性互动。可脱糖不等于免费,脱糖后的核心语义也可能暴露边界差异。语言设计者要问:该特性是否解决真实、常见的问题?是否和既有语言模型一致?能否以很小的实现复杂度获得显著可读性收益?

健康的语言通常不是完全无糖,也不是无限堆叠甜味。小而正交的核心加上少量高价值的表面便利,往往比庞杂的特殊规则更持久。