语法分析器实现方案全解

从手写递归下降到工业级 LALR(1) 生成器,全面对比各方案的核心原理、适用场景与实际代码示例

编译器/解释器完整流水线
阶段 1
词法分析
阶段 2 ← 本文重点
语法分析
阶段 3
语义分析
阶段 4
中间代码生成
阶段 5
目标代码生成

语法分析器(Parser)接收词法分析器产出的 Token 流,根据语言的文法规则验证语法合法性并构建 抽象语法树(AST)。 实现方式主要分为两大类:自顶向下(Top-Down)自底向上(Bottom-Up),每类下有多种具体方案。

★ 首选推荐

手写递归下降

每个文法非终结符对应一个递归函数,直接用任意编程语言实现,无需额外工具。

实现成本
可控性
常用

LL(*) / ANTLR

写文法描述,自动生成解析器代码。适合复杂语法规则多的场景,工业级方案。

实现成本
可控性
高级

LALR(1) / Bison

自底向上解析,文法表达能力最强,生成的解析器性能极高,但上手门槛高。

实现成本
性能
现代

PEG / 解析器组合子

无歧义的表达文法,或用函数式方式组合小解析单元,特别适合 Rust/Haskell 技术栈。

实现成本
歧义处理
核心选择原则: 没有绝对最好的方案,按实现成本从低到高、适用范围从窄到宽选择。 绝大多数场景首选手写递归下降;语法规则极复杂时选 ANTLR;极致性能需求选 LALR(1);函数式技术栈选解析器组合子。

真实项目用了哪种方案?

GCC / CPython / Go compiler
手写递归下降
最主流编程语言的官方编译器都手写递归下降,可控性和调试效率第一
Spark SQL / Hive / Presto
ANTLR LL(*)
大数据 SQL 引擎,文法复杂、需要多目标语言,ANTLR 生态完美匹配
MySQL / PostgreSQL
Bison LALR(1)
传统关系型数据库,追求极致解析性能和完整 SQL 标准覆盖
Rust nom / Python Lark
PEG / 组合子
现代语言生态的 DSL 和协议解析首选,零冲突、组合式开发体验极佳
TypeScript (tsc)
手写递归下降
Microsoft 官方完全手写,方便插入复杂的 TS 类型系统上下文逻辑
Lua 5.x
手写递归下降
嵌入式脚本语言,单文件即完整解析器,移植性极强
首选推荐 自顶向下 无需工具

递归下降分析法(Recursive Descent)

直接用编程语言手写,每个文法非终结符对应一个递归函数,最高效、最可控的语法分析方案

核心工作原理

前提条件:需先消除文法中的左递归(如 A → Aα)并提取公共左因子, 改写为右递归形式。规则固定,操作机械化,一次掌握终身受用。

Python 实现示例 — 算术表达式解析器

解析 1 + 2 * (3 - 4) 这类带括号和优先级的算术表达式,构建 AST:

Python
# 文法(已消除左递归):
# 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}")
    
优势
  • 无需任何第三方工具
  • 逻辑完全透明,调试极其简单
  • 可以在任意位置插入上下文逻辑
  • 错误报告精确,易于定制提示
  • 工业级编译器的主流选择
  • 支持任意目标语言
劣势
  • 需手动处理左递归消除
  • 复杂语法时代码量较大
  • 复杂歧义文法处理较繁琐
  • 文法规则散落在各函数中,整体结构不直观

适用场景

新手学习入门首选,一眼看懂解析原理
小型 DSL配置语言、模板语言、脚本解析
完整编程语言Go、Rust、TypeScript 编译器
需要上下文类型检查等语义逻辑直接内嵌
自顶向下 生成器工具 多语言支持

LL(*) 生成器 / ANTLR

写文法即生成解析器,SQL 引擎、复杂语法原型的不二之选

核心工作原理

LL(*) 是 LL(k) 的进化版,允许向前预看任意多个 Token(而非固定 k 个)来消除歧义。 ANTLR 4 实现了 Adaptive LL(*) 算法(ALL(*)),在运行时动态决策预看深度,几乎消除了传统 LL 文法的所有限制。 只需编写 .g4 文法文件,ANTLR 自动生成 Lexer、Parser、Listener/Visitor 代码。

ANTLR4 Grammar (.g4)
// 文件名: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 ;
  
Shell
# 生成 Python 版解析器
antlr4 -Dlanguage=Python3 -visitor Expr.g4

# 生成 Java 版(默认)
antlr4 -visitor Expr.g4

# 支持语言:Java / Python3 / Go / C# / C++ / Swift / JS / TypeScript...
Python(使用生成的解析器)
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。

Bison (.y 文法文件)
%{
/* 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; } ;
%%
移进/归约冲突:LALR(1) 最常见的坑。当同一状态下解析器无法决定是继续移进还是归约时产生冲突。 Bison 默认"优先移进",但需要通过优先级声明(%left/%right/%prec)来消除冲突,调试成本较高。
优势
  • 能处理几乎所有上下文无关文法
  • 文法限制比 LL 系列少
  • 生成的解析器性能最高(O(n) 严格)
  • 内存占用极低
  • 历史悠久,成熟稳定
劣势
  • 上手门槛高,需要理解 LR 原理
  • 移进/归约、归约/归约冲突难调试
  • 错误报告难以定制,提示不友好
  • 嵌入语义动作的 C 代码难以维护
  • 不如递归下降可读性强
无歧义 现代方案 函数式友好

PEG 文法 & 解析器组合子

无歧义表达文法 + 函数式组合方式,Rust/Haskell 生态的现代主流

PEG 核心原理

PEG(Parsing Expression Grammar)使用有序选择/ 而非 |),总是优先匹配第一个可以成功的选项,从根本上消除了文法歧义。 背后使用记忆化递归下降(Packrat Parsing)保证 O(n) 时间复杂度,用额外内存换取速度。

Rust — nom 解析器组合子
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)
}
Python — Lark (PEG 文法)
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 + 13 * (2 + 4 * 5)

点击"解析"按钮查看结果...
Token 流分析

查看词法分析阶段产出的 Token 序列

点击"词法分析"查看 Token 流...
PEG 有序选择演示

展示 PEG 有序选择 vs 传统 BNF 无序选择的匹配差异

输入一个词并点击"PEG 匹配"...

选择向导

回答几个问题,找到最适合你场景的解析器方案

你的主要目标是什么?

📚
学习编译原理 / 理解语法分析
想搞清楚编译器是怎么工作的,学习解析原理
⚙️
实现自定义 DSL 或脚本语言
配置语言、模板语法、自定义查询语言等
🏗️
解析复杂通用语言(SQL / 完整编程语言)
语法规则很多,想快速实现,不想手写大量代码
追求极致性能 / C/C++ 传统方案
对解析性能要求极高,或在历史 C/C++ 工具链中工作

你使用的技术栈是?

🦀
Rust / Haskell / 函数式语言
希望解析器与原生类型系统和所有权机制完美融合
🐍
Python / Java / Go / 其他通用语言
不依赖特定函数式范式,快速实现为主