引言
编译器前端是练手 Zig 的最佳项目之一:它需要大量使用标签联合(union(enum))、切片、错误联合类型、编译期反射与精细的内存管理——几乎覆盖了 Zig 语言的核心特性,同时又有清晰的正确性标准(能不能正确解析并求值)。
本文实现一个支持变量、算术、比较、函数调用的迷你语言,完整走通「源码 → Token → AST → 求值」四步。代码基于 Zig 0.13/0.14,可直接编译运行。
目录
- 1. 编译器前端全景
- 2. 词法分析器设计
- 3. 手写词法分析实现
- 4. 抽象语法树设计
- 5. 递归下降解析
- 6. Pratt 表达式解析
- 7. 符号表与语义分析
- 8. 错误报告与错误恢复
- 9. 树遍历解释器
- 速查表
- 一句话记忆
- 相关阅读
- 延伸阅读
1. 编译器前端全景
1.1 四个阶段
源码字符串
↓ 词法分析 Lexer
Token 流
↓ 语法分析 Parser
抽象语法树 AST
↓ 语义分析 Analyzer
带类型标注的 AST / IR
↓ 求值或代码生成
结果
词法分析负责把字符流切成有意义的 Token;语法分析负责把 Token 组织成树;语义分析负责解析名字、检查类型;后端负责执行或生成目标代码。
1.2 目标语言定义
本文实现的 Mini 语言支持:
let x = 10;
let y = x * 2 + 3;
if (y > 20) { print(y); }
fn add(a, b) { return a + b; }
print(add(x, y));
运算符优先级:= < ==/!=/</> < +/- < *// < 一元 - < 调用/分组。
2. 词法分析器设计
2.1 Token 类型
用标签联合表示 Token,这是 Zig 表达「带数据的不同类别」的惯用法:
const TokenTag = enum {
number, string, ident,
kw_let, kw_fn, kw_if, kw_else, kw_return, kw_print,
plus, minus, star, slash,
eq, eq_eq, bang_eq, lt, gt,
lparen, rparen, lbrace, rbrace, comma, semicolon,
eof,
};
const Token = struct {
tag: TokenTag,
lexeme: []const u8, // 指向源码缓冲区,零拷贝
line: u32,
column: u32,
};
2.2 关键字识别
用 std.StaticStringMap 在编译期建表,查表零运行时初始化成本:
const keywords = std.StaticStringMap(TokenTag).initComptime(.{
.{ "let", .kw_let },
.{ "fn", .kw_fn },
.{ "if", .kw_if },
.{ "print", .kw_print },
});
3. 手写词法分析实现
3.1 扫描器骨架
const Lexer = struct {
src: []const u8,
pos: usize = 0,
line: u32 = 1,
col: u32 = 1,
fn peek(self: *Lexer) u8 {
return if (self.pos < self.src.len) self.src[self.pos] else 0;
}
fn advance(self: *Lexer) u8 {
const ch = self.src[self.pos];
self.pos += 1;
if (ch == '\n') {
self.line += 1;
self.col = 1;
} else {
self.col += 1;
}
return ch;
}
};
3.2 主循环
fn next(self: *Lexer) !Token {
self.skipWhitespace();
if (self.pos >= self.src.len) return self.make(.eof, self.pos, self.pos);
const start = self.pos;
const ch = self.advance();
if (std.ascii.isDigit(ch)) return self.number(start);
if (std.ascii.isAlphabetic(ch) or ch == '_') return self.ident(start);
return switch (ch) {
'+' => self.make(.plus, start, self.pos),
'-' => self.make(.minus, start, self.pos),
'*' => self.make(.star, start, self.pos),
'/' => self.make(.slash, start, self.pos),
'(' => self.make(.lparen, start, self.pos),
')' => self.make(.rparen, start, self.pos),
'{' => self.make(.lbrace, start, self.pos),
'}' => self.make(.rbrace, start, self.pos),
',' => self.make(.comma, start, self.pos),
';' => self.make(.semicolon, start, self.pos),
'=' => if (self.peek() == '=')
self.twoChar(.eq_eq, start)
else
self.make(.eq, start, self.pos),
'!' => if (self.peek() == '=')
self.twoChar(.bang_eq, start)
else
return error.UnexpectedCharacter,
'<' => self.make(.lt, start, self.pos),
'>' => self.make(.gt, start, self.pos),
else => error.UnexpectedCharacter,
};
}
4. 抽象语法树设计
4.1 节点定义
AST 同样是标签联合。表达式与语句分开定义,避免巨型联合:
const Expr = union(enum) {
number: f64,
string: []const u8,
ident: []const u8,
unary: struct { op: TokenTag, operand: *Expr },
binary: struct { op: TokenTag, lhs: *Expr, rhs: *Expr },
call: struct { callee: []const u8, args: []*Expr },
};
const Stmt = union(enum) {
let: struct { name: []const u8, value: *Expr },
assign: struct { name: []const u8, value: *Expr },
print: *Expr,
expr: *Expr,
if_stmt: struct { cond: *Expr, then: *Stmt },
func: struct { name: []const u8, params: [][]const u8, body: []*Stmt },
return_stmt: ?*Expr,
};
const Program = struct { stmts: []*Stmt };
4.2 用 arena 分配节点
AST 节点数量多、生命周期统一(整个编译期),是 arena 分配器的典型场景:
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const a = arena.allocator();
const node = try a.create(Expr);
node.* = .{ .number = 42.0 };
一次 deinit 释放全部节点,无需逐个追踪。
5. 递归下降解析
5.1 解析器骨架
const Parser = struct {
tokens: []Token,
pos: usize = 0,
alloc: std.mem.Allocator,
fn peek(self: *Parser) Token {
return self.tokens[self.pos];
}
fn check(self: *Parser, tag: TokenTag) bool {
return self.peek().tag == tag;
}
fn match(self: *Parser, tag: TokenTag) bool {
if (self.check(tag)) {
self.pos += 1;
return true;
}
return false;
}
fn expect(self: *Parser, tag: TokenTag) !Token {
if (!self.check(tag)) {
std.debug.print("第 {d} 行: 期望 {s},实际 {s}\n", .{
self.peek().line, @tagName(tag), @tagName(self.peek().tag),
});
return error.UnexpectedToken;
}
const t = self.peek();
self.pos += 1;
return t;
}
};
5.2 语句解析
fn parseStmt(self: *Parser) anyerror!*Stmt {
if (self.match(.kw_let)) {
const name = try self.expect(.ident);
_ = try self.expect(.eq);
const value = try self.parseExpr(0);
_ = try self.expect(.semicolon);
const s = try self.alloc.create(Stmt);
s.* = .{ .let = .{ .name = name.lexeme, .value = value } };
return s;
}
if (self.match(.kw_print)) {
_ = try self.expect(.lparen);
const e = try self.parseExpr(0);
_ = try self.expect(.rparen);
_ = try self.expect(.semicolon);
const s = try self.alloc.create(Stmt);
s.* = .{ .print = e };
return s;
}
if (self.match(.kw_if)) {
_ = try self.expect(.lparen);
const cond = try self.parseExpr(0);
_ = try self.expect(.rparen);
const then = try self.parseStmt();
const s = try self.alloc.create(Stmt);
s.* = .{ .if_stmt = .{ .cond = cond, .then = then } };
return s;
}
const e = try self.parseExpr(0);
_ = try self.expect(.semicolon);
const s = try self.alloc.create(Stmt);
s.* = .{ .expr = e };
return s;
}
6. Pratt 表达式解析
6.1 优先级表
Pratt 解析(运算符优先级解析)用一张表统一处理二元运算的优先级与结合性:
fn bindingPower(tag: TokenTag) struct { left: u8, right: u8 } {
return switch (tag) {
.eq => .{ .left = 1, .right = 1 }, // 右结合
.eq_eq, .bang_eq, .lt, .gt => .{ .left = 2, .right = 3 },
.plus, .minus => .{ .left = 4, .right = 5 },
.star, .slash => .{ .left = 6, .right = 7 },
else => .{ .left = 0, .right = 0 },
};
}
左结合运算符 left < right,右结合则 left == right。
6.2 核心循环
fn parseExpr(self: *Parser, min_bp: u8) anyerror!*Expr {
var lhs = try self.parsePrefix();
while (true) {
const bp = bindingPower(self.peek().tag);
if (bp.left <= min_bp) break;
const op = self.peek().tag;
self.pos += 1;
const rhs = try self.parseExpr(bp.right);
const node = try self.alloc.create(Expr);
node.* = .{ .binary = .{ .op = op, .lhs = lhs, .rhs = rhs } };
lhs = node;
}
return lhs;
}
6.3 前缀解析
fn parsePrefix(self: *Parser) anyerror!*Expr {
if (self.match(.minus)) {
const operand = try self.parseExpr(7); // 一元优先级最高
const node = try self.alloc.create(Expr);
node.* = .{ .unary = .{ .op = .minus, .operand = operand } };
return node;
}
return self.parsePrimary();
}
fn parsePrimary(self: *Parser) anyerror!*Expr {
const t = self.peek();
switch (t.tag) {
.number => {
self.pos += 1;
const node = try self.alloc.create(Expr);
node.* = .{ .number = try std.fmt.parseFloat(f64, t.lexeme) };
return node;
},
.ident => {
self.pos += 1;
if (self.match(.lparen)) {
var args = std.ArrayList(*Expr).init(self.alloc);
if (!self.check(.rparen)) {
while (true) {
try args.append(try self.parseExpr(0));
if (!self.match(.comma)) break;
}
}
_ = try self.expect(.rparen);
const node = try self.alloc.create(Expr);
node.* = .{ .call = .{ .callee = t.lexeme, .args = try args.toOwnedSlice() } };
return node;
}
const node = try self.alloc.create(Expr);
node.* = .{ .ident = t.lexeme };
return node;
},
.lparen => {
self.pos += 1;
const e = try self.parseExpr(0);
_ = try self.expect(.rparen);
return e;
},
else => return error.ExpectedExpression,
}
}
7. 符号表与语义分析
7.1 作用域链
const Scope = struct {
alloc: std.mem.Allocator,
vars: std.StringHashMapUnmanaged(Value) = .{},
parent: ?*Scope = null,
fn lookup(self: *Scope, name: []const u8) ?*Value {
if (self.vars.getPtr(name)) |v| return v;
if (self.parent) |p| return p.lookup(name);
return null;
}
};
7.2 静态检查
求值前遍历 AST 做一轮检查,能在运行前捕获大部分错误:变量是否已声明、函数调用参数个数是否匹配、return 是否在函数体内。
把能静态发现的错误提前,能显著改善使用体验——这也正是 Zig 本身「尽可能在编译期发现问题」理念的延伸。
8. 错误报告与错误恢复
8.1 恐慌模式恢复
遇到语法错误时,不要立即放弃整个文件,而是跳到下一个同步点(; 或 })继续解析,一次报告多个错误:
fn synchronize(self: *Parser) void {
while (!self.check(.eof)) {
if (self.tokens[self.pos - 1].tag == .semicolon) return;
switch (self.peek().tag) {
.kw_let, .kw_fn, .kw_if, .kw_print => return,
else => self.pos += 1,
}
}
}
好的错误信息包含三点:在哪出错、期望什么、实际看到什么。加上源码片段与插入符,用户几乎不需要思考就能定位问题。
9. 树遍历解释器
9.1 值类型与表达式求值
const Value = union(enum) {
nil,
number: f64,
string: []const u8,
boolean: bool,
};
fn evalExpr(e: *Expr, scope: *Scope) anyerror!Value {
return switch (e.*) {
.number => |n| .{ .number = n },
.string => |s| .{ .string = s },
.ident => |name| (scope.lookup(name) orelse return error.UndefinedVariable).*,
.unary => |u| .{ .number = -(try evalExpr(u.operand, scope)).number },
.binary => |b| blk: {
const l = try evalExpr(b.lhs, scope);
const r = try evalExpr(b.rhs, scope);
break :blk switch (b.op) {
.plus => .{ .number = l.number + r.number },
.minus => .{ .number = l.number - r.number },
.star => .{ .number = l.number * r.number },
.slash => .{ .number = l.number / r.number },
.gt => .{ .boolean = l.number > r.number },
.lt => .{ .boolean = l.number < r.number },
.eq_eq => .{ .boolean = l.number == r.number },
else => error.UnsupportedOperator,
};
},
.call => error.NotImplemented,
};
}
9.2 语句执行
fn exec(stmt: *Stmt, scope: *Scope) anyerror!void {
switch (stmt.*) {
.let => |l| {
const v = try evalExpr(l.value, scope);
try scope.vars.put(scope.alloc, l.name, v);
},
.print => |e| {
const v = try evalExpr(e, scope);
switch (v) {
.number => |n| std.debug.print("{d}\n", .{n}),
.string => |s| std.debug.print("{s}\n", .{s}),
.boolean => |b| std.debug.print("{}\n", .{b}),
.nil => std.debug.print("nil\n", .{}),
}
},
.if_stmt => |i| {
const cond = try evalExpr(i.cond, scope);
if (cond.boolean) try exec(i.then, scope);
},
else => {},
}
}
速查表
| 阶段 | 关键结构 | Zig 特性 |
|---|---|---|
| 词法分析 | Token(tag + lexeme + 位置) | enum、切片、StaticStringMap |
| 语法分析 | Expr / Stmt(标签联合) | union(enum)、*T 间接层 |
| 表达式 | Pratt 优先级表 | switch 穷尽匹配 |
| 语义分析 | 作用域链 Scope | StringHashMap、可选指针 |
| 内存 | 节点统一 arena 分配 | ArenaAllocator |
| 错误 | 带行列的错误结构 | error{} 集合、try |
| 求值 | 树遍历解释器 | 标签联合的模式匹配 |
一句话记忆
前端四步:Lexer 把字符切成 Token(保留位置切片)、Parser 用递归下降 + Pratt 把 Token 组成 AST(节点用 arena 分配、递归处用指针)、Analyzer 查符号表、后端遍历 AST 求值;错误要带行列并做恐慌恢复。
相关阅读
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。