语法分析器(Parser)接收词法分析器产出的 Token 流,根据语言的文法规则验证语法合法性并构建 抽象语法树(AST)。 实现方式主要分为两大类:自顶向下(Top-Down) 和 自底向上(Bottom-Up),每类下有多种具体方案。
手写递归下降
每个文法非终结符对应一个递归函数,直接用任意编程语言实现,无需额外工具。
LL(*) / ANTLR
写文法描述,自动生成解析器代码。适合复杂语法规则多的场景,工业级方案。
LALR(1) / Bison
自底向上解析,文法表达能力最强,生成的解析器性能极高,但上手门槛高。
PEG / 解析器组合子
无歧义的表达文法,或用函数式方式组合小解析单元,特别适合 Rust/Haskell 技术栈。
真实项目用了哪种方案?
递归下降分析法(Recursive Descent)
直接用编程语言手写,每个文法非终结符对应一个递归函数,最高效、最可控的语法分析方案
核心工作原理
- 文法中的每个非终结符(如 expr、stmt、term)都对应一个解析函数
- 函数从 Token 流当前位置开始,向前预看(Lookahead)决定选用哪条产生式
- 递归调用子函数解析子表达式,直到遇到终结符(Token)即消费它
- 每个函数成功时返回对应 AST 节点,失败时抛出语法错误
Python 实现示例 — 算术表达式解析器
解析 1 + 2 * (3 - 4) 这类带括号和优先级的算术表达式,构建 AST:
# 文法(已消除左递归): # expr → term (('+' | '-') term)* # term → factor (('*' | '/') factor)* # factor → NUMBER | '(' expr ')' from dataclasses import dataclass from typing import Union @dataclass class BinOp: op: str left: Union['BinOp', int] right: Union['BinOp', int] class RecursiveDescentParser: def __init__(self, tokens: list): self.tokens = tokens self.pos = 0 def peek(self): return self.tokens[self.pos] if self.pos < len(self.tokens) else None def consume(self, expected=None): tok = self.peek() if expected and tok != expected: raise SyntaxError(f"期望 {expected},但得到 {tok}") self.pos += 1 return tok def parse_expr(self): # expr → term (('+' | '-') term)* left = self.parse_term() while self.peek() in ('+', '-'): op = self.consume() left = BinOp(op, left, self.parse_term()) return left def parse_term(self): # term → factor (('*' | '/') factor)* left = self.parse_factor() while self.peek() in ('*', '/'): op = self.consume() left = BinOp(op, left, self.parse_factor()) return left def parse_factor(self): # factor → NUMBER | '(' expr ')' tok = self.peek() if isinstance(tok, int): return self.consume() elif tok == '(': self.consume('(') node = self.parse_expr() # 递归调用! self.consume(')') return node raise SyntaxError(f"意外的 token: {tok}")
- 无需任何第三方工具
- 逻辑完全透明,调试极其简单
- 可以在任意位置插入上下文逻辑
- 错误报告精确,易于定制提示
- 工业级编译器的主流选择
- 支持任意目标语言
- 需手动处理左递归消除
- 复杂语法时代码量较大
- 复杂歧义文法处理较繁琐
- 文法规则散落在各函数中,整体结构不直观
适用场景
LL(*) 生成器 / ANTLR
写文法即生成解析器,SQL 引擎、复杂语法原型的不二之选
核心工作原理
LL(*) 是 LL(k) 的进化版,允许向前预看任意多个 Token(而非固定 k 个)来消除歧义。
ANTLR 4 实现了 Adaptive LL(*) 算法(ALL(*)),在运行时动态决策预看深度,几乎消除了传统 LL 文法的所有限制。
只需编写 .g4 文法文件,ANTLR 自动生成 Lexer、Parser、Listener/Visitor 代码。
// 文件名:Expr.g4 — 算术表达式文法 grammar Expr; // Parser 规则(小写开头) prog : stat+ ; stat : expr NEWLINE | ID '=' expr NEWLINE | NEWLINE ; expr : expr ('*'|'/') expr // 乘除(高优先级) | expr ('+'|'-') expr // 加减(低优先级) | '(' expr ')' | INT | ID ; // Lexer 规则(大写开头) ID : [a-zA-Z]+ ; INT : [0-9]+ ; NEWLINE : '\r'? '\n' ; WS : [ \t]+ -> skip ;
# 生成 Python 版解析器 antlr4 -Dlanguage=Python3 -visitor Expr.g4 # 生成 Java 版(默认) antlr4 -visitor Expr.g4 # 支持语言:Java / Python3 / Go / C# / C++ / Swift / JS / TypeScript...
from antlr4 import * from ExprLexer import ExprLexer from ExprParser import ExprParser class EvalVisitor(ExprVisitor): def visitExpr(self, ctx): if ctx.INT(): return int(ctx.INT().getText()) left = self.visit(ctx.expr(0)) op = ctx.getChild(1).getText() right = self.visit(ctx.expr(1)) return {'+': left+right, '-': left-right, '*': left*right, '/': left//right}[op] input_stream = InputStream("3 + 4 * 2\n") parser = ExprParser(CommonTokenStream(ExprLexer(input_stream))) print(EvalVisitor().visit(parser.prog())) # → 11
- 只写文法,自动生成解析器代码
- 支持 10+ 种目标语言
- 自带 Listener/Visitor 两种遍历模式
- 现成文法库(SQL/Java/JS/C++ ...)
- ALL(*) 算法可处理几乎所有 LL 文法
- 自带 ANTLRworks 可视化调试工具
- 需要学习 ANTLR 工具链和语法
- 可控性低于手写,极端定制成本高
- 生成代码体积较大
- 运行时 ATN 预测有额外开销
LALR(1) 生成器 / Bison & Yacc
自底向上、极致性能,传统数据库和 C/C++ 编译器的历史选择
核心工作原理
LALR(1) 是 LR(1) 的合并优化版。解析器维护一个状态栈,通过移进(Shift)和归约(Reduce)两个操作驱动。
每步只需向前预看 1 个 Token,查表决定操作,解析时间严格为 O(n),内存占用极小。
Bison 读取文法 .y 文件,生成 C/C++ 驱动的 Parser。
%{ /* C 头文件引用 */ #include <stdio.h> #include <stdlib.h> int yylex(void); void yyerror(const char *); %} /* 声明运算符优先级(从低到高)*/ %left '+' '-' %left '*' '/' %right UMINUS /* 一元负号,最高优先级 */ %% /* 产生式规则 */ input : /* 空 */ | input line ; line : '\n' | expr '\n' { printf("= %d\n", $1); } ; expr : expr '+' expr { $$ = $1 + $3; } | expr '-' expr { $$ = $1 - $3; } | expr '*' expr { $$ = $1 * $3; } | expr '/' expr { $$ = $3 ? $1/$3 : (yyerror("除零错误"),0); } | '(' expr ')' { $$ = $2; } | '-' expr %prec UMINUS { $$ = -$2; } | NUM { $$ = $1; } ; %%
%left/%right/%prec)来消除冲突,调试成本较高。
- 能处理几乎所有上下文无关文法
- 文法限制比 LL 系列少
- 生成的解析器性能最高(O(n) 严格)
- 内存占用极低
- 历史悠久,成熟稳定
- 上手门槛高,需要理解 LR 原理
- 移进/归约、归约/归约冲突难调试
- 错误报告难以定制,提示不友好
- 嵌入语义动作的 C 代码难以维护
- 不如递归下降可读性强
PEG 文法 & 解析器组合子
无歧义表达文法 + 函数式组合方式,Rust/Haskell 生态的现代主流
PEG 核心原理
PEG(Parsing Expression Grammar)使用有序选择(/ 而非 |),总是优先匹配第一个可以成功的选项,从根本上消除了文法歧义。
背后使用记忆化递归下降(Packrat Parsing)保证 O(n) 时间复杂度,用额外内存换取速度。
use nom::{branch::alt, bytes::complete::tag, character::complete::{digit1, space0}, combinator::map, sequence::delimited, IResult}; #[derive(Debug)] enum Expr { Num(i64), Add(Box<Expr>, Box<Expr>) } // 解析数字 fn parse_num(i: &str) -> IResult<&str, Expr> { map(digit1, |s: &str| Expr::Num(s.parse().unwrap()))(i) } // 解析加法 "num + num"(组合子无缝拼接) fn parse_add(i: &str) -> IResult<&str, Expr> { let (i, left) = parse_num(i)?; let (i, _) = delimited(space0, tag("+"), space0)(i)?; let (i, right) = parse_num(i)?; Ok((i, Expr::Add(Box::new(left), Box::new(right)))) } // 最终入口:先尝试加法,再尝试纯数字 fn parse_expr(i: &str) -> IResult<&str, Expr> { alt((parse_add, parse_num))(i) }
from lark import Lark, Transformer grammar = r""" expr: expr "+" term -> add // 有序选择,无歧义 | term term: term "*" atom -> mul | atom atom: NUMBER -> number | "(" expr ")" NUMBER: /[0-9]+/ %ignore /\s+/ """ class Calc(Transformer): def add(self, items): return items[0] + items[1] def mul(self, items): return items[0] * items[1] def number(self, items): return int(items[0]) parser = Lark(grammar, start='expr', parser='earley') print(Calc().transform(parser.parse("3 + 4 * 2"))) # → 11
- 有序选择,文法无歧义
- 组合子与原生代码无缝集成
- Packrat 保证 O(n) 性能
- 错误消息清晰,便于定制
- Rust/Haskell 等函数式技术栈首选
- Packrat 内存消耗比传统方案高
- 有序选择有时会掩盖真实文法意图
- 不如 ANTLR 生态成熟丰富
- 极复杂文法时调试仍有难度
横向对比
4 种主流方案在各维度上的全面对比
| 方案 | 解析方向 | 入门难度 | 实现成本 | 文法可控性 | 运行性能 | 错误提示 | 依赖工具 | 典型代表 |
|---|---|---|---|---|---|---|---|---|
| 手写递归下降 | 自顶向下 | 低 | 无 | GCC, CPython, Go, Rust, TypeScript | ||||
| LL(*) / ANTLR | 自顶向下 | 中 | ANTLR 工具 | Spark SQL, Hive, Presto | ||||
| LALR(1) / Bison | 自底向上 | 高 | Bison/Yacc | MySQL, PostgreSQL, PHP 早期版本 | ||||
| PEG / 解析器组合子 | 自顶向下 | 中 | nom/pest/Lark | nom (Rust), Parsec (Haskell) |
文法表达能力对比
| 特性 | 递归下降 | LL(*) / ANTLR | LALR(1) / Bison | PEG |
|---|---|---|---|---|
| 处理左递归 | ✗ 需手动消除 | △ ALL(*) 支持 | ✓ 原生支持 | ✗ 需改写 |
| 处理歧义文法 | △ 可手动处理 | △ 需优先级声明 | △ 冲突解决机制 | ✓ 有序选择无歧义 |
| 上下文相关语法 | ✓ 直接内嵌代码 | △ 需 Action 机制 | △ 需 Action 机制 | △ 部分支持 |
| 运算符优先级 | ✓ 通过函数层级 | ✓ 产生式自然表达 | ✓ %left/%right 声明 | ✓ 有序匹配 |
| 向前预看深度 | 任意(手控) | 任意(动态) | 固定 1 个 | 任意(回溯) |
场景推荐矩阵
| 使用场景 | 首选方案 | 备选方案 | 理由 |
|---|---|---|---|
| 学习编译原理 | 手写递归下降 | — | 最接近理论,逻辑透明 |
| 小型配置 DSL | 手写递归下降 | PEG / Lark | 简单快速,零依赖 |
| 工业级编程语言 | 手写递归下降 | — | GCC/Clang/Go 的实际选择 |
| SQL 解析引擎 | ANTLR LL(*) | 手写递归下降 | 文法复杂 + 现成 SQL g4 文件 |
| 复杂语言原型 | ANTLR LL(*) | 手写递归下降 | 快速验证文法,减少重复代码 |
| C/C++ 传统解析 | LALR(1) / Bison | 手写递归下降 | 极致性能,历史积累丰富 |
| Rust 生态 DSL | nom (PEG 组合子) | pest (PEG) | 与 Rust 类型系统完美融合 |
| 协议/二进制解析 | 解析器组合子 | 手写递归下降 | 字节级精确控制,零拷贝 |
交互演示
在浏览器中即时运行递归下降解析器,观察 AST 构建过程
支持四则运算和括号,如:1 + 2 * 3、(10 - 4) / 2 + 1、3 * (2 + 4 * 5)
查看词法分析阶段产出的 Token 序列
展示 PEG 有序选择 vs 传统 BNF 无序选择的匹配差异
选择向导
回答几个问题,找到最适合你场景的解析器方案
你的主要目标是什么?
你使用的技术栈是?