第 8 章:语句与状态(Statements and State)
原文:Robert Nystrom, Crafting Interpreters, Chapter 8。原书以 CC BY-NC-SA 4.0 协议发布;本译文用于学习与研究。
人的本性有一种难以理解的状态,是不断改变一切;又有一种同样难以理解的状态,是不断努力使一切保持不变。
Charles Dickens

表达式计算值,但程序若没有状态和可观察效果,能做的事情很有限。本章加入语句、变量、赋值与块作用域。至此 Lox 可以保存值、修改值、向用户输出,并在嵌套环境中管理名称。
8.1 语句(Statements)
表达式的结果是值,语句的结果是效果。首先支持两种语句:
print "Hello, world!";
1 + 2;
第一条是打印语句,第二条是表达式语句。后者计算表达式但丢弃值;它看似无用,却为函数调用、赋值等未来有副作用的表达式提供统一位置。
8.1.1 语句语法树(Statement Syntax Trees)
与表达式一样,为语句创建独立基类 Stmt。更新 AST 生成器,生成 Expression 与 Print 两种节点:
defineAst(outputDir, "Stmt", Arrays.asList(
"Expression : Expr expression",
"Print : Expr expression"
));
生成出的 Stmt 也拥有访问者接口:
abstract class Stmt {
interface Visitor<R> {
R visitExpressionStmt(Expression stmt);
R visitPrintStmt(Print stmt);
}
static class Expression extends Stmt {
final Expr expression;
Expression(Expr expression) { this.expression = expression; }
@Override <R> R accept(Visitor<R> visitor) {
return visitor.visitExpressionStmt(this);
}
}
static class Print extends Stmt {
final Expr expression;
Print(Expr expression) { this.expression = expression; }
@Override <R> R accept(Visitor<R> visitor) {
return visitor.visitPrintStmt(this);
}
}
abstract <R> R accept(Visitor<R> visitor);
}
表达式与语句分成两棵层次结构,是因为它们在语言中有不同角色:表达式嵌在更大表达式中并产生值;语句按顺序执行并产生效果。
8.1.2 解析语句(Parsing Statements)
程序入口不再只解析表达式,而是解析声明或语句:
List<Stmt> parse() {
List<Stmt> statements = new ArrayList<>();
while (!isAtEnd()) {
statements.add(statement());
}
return statements;
}
private Stmt statement() {
if (match(PRINT)) return printStatement();
return expressionStatement();
}
private Stmt printStatement() {
Expr value = expression();
consume(SEMICOLON, "Expect ';' after value.");
return new Stmt.Print(value);
}
private Stmt expressionStatement() {
Expr expr = expression();
consume(SEMICOLON, "Expect ';' after expression.");
return new Stmt.Expression(expr);
}
分号在此处成为重要同步点:它既终止语句,也可在错误恢复中作为重新开始的边界。
8.1.3 执行语句(Executing Statements)
解释器同时实现 Expr.Visitor<Object> 与 Stmt.Visitor<Void>。表达式访问者返回值,语句访问者返回 Void:
class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
void interpret(List<Stmt> statements) {
try {
for (Stmt statement : statements) {
execute(statement);
}
} catch (RuntimeError error) {
Lox.runtimeError(error);
}
}
private void execute(Stmt stmt) {
stmt.accept(this);
}
@Override
public Void visitExpressionStmt(Stmt.Expression stmt) {
evaluate(stmt.expression);
return null;
}
@Override
public Void visitPrintStmt(Stmt.Print stmt) {
Object value = evaluate(stmt.expression);
System.out.println(stringify(value));
return null;
}
}
stringify() 把内部值转换为用户可读的 Lox 输出:
private String stringify(Object object) {
if (object == null) return "nil";
if (object instanceof Double) {
String text = object.toString();
if (text.endsWith(".0")) {
text = text.substring(0, text.length() - 2);
}
return text;
}
return object.toString();
}
这让 print 1.0; 显示为 1,符合语言表面风格而不是 Java 的装箱表示。
8.2 全局变量(Global Variables)
变量声明语法为:
var beverage = "espresso";
var breakfast;
初始化器可省略,省略时值为 nil。读取变量只是一个新的表达式:
primary -> ... | IDENTIFIER ;
8.2.1 变量 AST 与解析
增加两种表达式/语句节点:
defineAst(outputDir, "Expr", Arrays.asList(
// 既有节点省略。
"Variable : Token name"
));
defineAst(outputDir, "Stmt", Arrays.asList(
// 既有节点省略。
"Var : Token name, Expr initializer"
));
解析器先判断是否为 var 声明。把 declaration() 与 statement() 分开,日后可以加入函数与类声明:
private Stmt declaration() {
try {
if (match(VAR)) return varDeclaration();
return statement();
} catch (ParseError error) {
synchronize();
return null;
}
}
private Stmt varDeclaration() {
Token name = consume(IDENTIFIER, "Expect variable name.");
Expr initializer = null;
if (match(EQUAL)) {
initializer = expression();
}
consume(SEMICOLON, "Expect ';' after variable declaration.");
return new Stmt.Var(name, initializer);
}
primary() 匹配标识符时构造变量表达式:
if (match(IDENTIFIER)) {
return new Expr.Variable(previous());
}
8.3 环境(Environments)
变量把名称映射为值。单独用一个 Environment 类持有这个映射,而不把表直接塞进解释器,能为后续作用域和闭包建立自然模型:

package com.craftinginterpreters.lox;
import java.util.HashMap;
import java.util.Map;
class Environment {
private final Map<String, Object> values = new HashMap<>();
void define(String name, Object value) {
values.put(name, value);
}
Object get(Token name) {
if (values.containsKey(name.lexeme)) {
return values.get(name.lexeme);
}
throw new RuntimeError(name,
"Undefined variable '" + name.lexeme + "'.");
}
}
使用 containsKey() 而不是仅检查 get() 返回值很重要:变量可合法保存 nil(Java 的 null),而未定义名称也会令 get() 返回 null。
解释器拥有全局环境:
private final Environment environment = new Environment();
@Override
public Void visitVarStmt(Stmt.Var stmt) {
Object value = null;
if (stmt.initializer != null) {
value = evaluate(stmt.initializer);
}
environment.define(stmt.name.lexeme, value);
return null;
}
@Override
public Object visitVariableExpr(Expr.Variable expr) {
return environment.get(expr.name);
}
初始化器在名称定义前求值,因此 var a = a; 会报告未定义变量;这避免了看似存在但尚无值的名称。
8.4 赋值(Assignment)
赋值既修改状态又产生被赋的值:
var a = "before";
a = "after";
print a;
赋值优先级低且右结合,故扩展文法:
expression -> assignment ;
assignment -> IDENTIFIER "=" assignment
| equality ;
不能直接按这份规则递归下降,因为它左侧以 IDENTIFIER 开头却还需先解析普通 equality。解决办法是先解析 equality,若随后看到 =,再验证左侧树确实为变量:
private Expr assignment() {
Expr expr = equality();
if (match(EQUAL)) {
Token equals = previous();
Expr value = assignment();
if (expr instanceof Expr.Variable) {
Token name = ((Expr.Variable) expr).name;
return new Expr.Assign(name, value);
}
error(equals, "Invalid assignment target.");
}
return expr;
}
注意右值递归调用 assignment(),因此 a = b = c 解析为 a = (b = c)。在 AST 中添加:
"Assign : Token name, Expr value"
环境增加赋值方法:
void assign(Token name, Object value) {
if (values.containsKey(name.lexeme)) {
values.put(name.lexeme, value);
return;
}
throw new RuntimeError(name,
"Undefined variable '" + name.lexeme + "'.");
}
解释赋值时先计算右侧,更新环境,再返回该值:
@Override
public Object visitAssignExpr(Expr.Assign expr) {
Object value = evaluate(expr.value);
environment.assign(expr.name, value);
return value;
}
返回赋值值让 print a = 3; 这样的表达式有效,也使链式赋值自然工作。
8.5 作用域(Scope)
全局表不够。块应引入新作用域:
var a = "global";
{
var a = "block";
print a; // block
}
print a; // global
内层同名变量遮蔽(shadow)外层变量。通过让每个环境指向一个可选外层环境,查找可以逐级向外:



class Environment {
final Environment enclosing;
private final Map<String, Object> values = new HashMap<>();
Environment() { enclosing = null; }
Environment(Environment enclosing) { this.enclosing = enclosing; }
Object get(Token name) {
if (values.containsKey(name.lexeme)) return values.get(name.lexeme);
if (enclosing != null) return enclosing.get(name);
throw new RuntimeError(name, "Undefined variable '" + name.lexeme + "'.");
}
void assign(Token name, Object value) {
if (values.containsKey(name.lexeme)) {
values.put(name.lexeme, value);
return;
}
if (enclosing != null) {
enclosing.assign(name, value);
return;
}
throw new RuntimeError(name, "Undefined variable '" + name.lexeme + "'.");
}
}
8.5.1 块的语法与语义(Block Syntax and Semantics)
块语句是花括号之间的声明列表:
private Stmt statement() {
if (match(PRINT)) return printStatement();
if (match(LEFT_BRACE)) return new Stmt.Block(block());
return expressionStatement();
}
private List<Stmt> block() {
List<Stmt> statements = new ArrayList<>();
while (!check(RIGHT_BRACE) && !isAtEnd()) {
statements.add(declaration());
}
consume(RIGHT_BRACE, "Expect '}' after block.");
return statements;
}
AST 添加 "Block : List<Stmt> statements"。解释器切换到新的环境执行块,并且无论是否抛出异常都必须恢复旧环境:
@Override
public Void visitBlockStmt(Stmt.Block stmt) {
executeBlock(stmt.statements, new Environment(environment));
return null;
}
void executeBlock(List<Stmt> statements, Environment environment) {
Environment previous = this.environment;
try {
this.environment = environment;
for (Stmt statement : statements) execute(statement);
} finally {
this.environment = previous;
}
}
这里 environment 字段不能再是 final。finally 是关键:若块中发生运行时错误或未来的 return、break 等非局部控制流,解释器仍要返回正确的外围环境。
挑战(Challenges)
- 当前块允许空块。是否应允许?尝试在解析器中禁止它,并评估错误消息是否更好。
- 增加逗号分隔的多个变量声明,例如
var a = 1, b = 2;。语义和错误恢复应如何设计? - 让未初始化变量在读取时产生错误,而不是保存
nil。这需要环境存储额外状态还是特殊哨兵值? - 实现一条
let声明,其变量只能赋值一次。赋值检查应放在解析、环境还是解释器中?
设计笔记:隐式变量声明(Implicit Variable Declaration)
Lox 要求 var 显式声明变量。也有语言会在第一次赋值时隐式创建变量,例如早期 BASIC、某些脚本语言,或允许按配置放宽声明规则的语言。隐式声明减少输入,却会把拼写错误变成悄悄创建的新变量,错误往往在很久后才显现。
显式声明让读者立刻看出名称首次出现的位置,也便于作用域分析、重构工具和错误诊断。隐式声明在快速实验和 REPL 中很方便,但大型程序会因隐藏的全局状态与拼写疏漏而受损。一些语言采取折中:默认要求声明,提供局部显式指令或严格模式来控制规则。
这并非单纯的“方便”与“安全”之争。变量声明是程序中名字、生命周期和意图的文档。选择时要考虑目标用户、代码规模、工具支持和语言是否鼓励临时探索。