mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2044 字
6 分钟
语法分析:从 Token 到 AST
2025-12-13

语法分析是编译器的第二个阶段——它接收词法分析器输出的 Token 流,构建出结构化的抽象语法树(AST)。如果说词法分析是”认字”,语法分析就是”断句”——理解 Token 之间的结构关系。

接下来拆解语法分析——上下文无关文法如何描述语言结构?LL 分析和 LR 分析有什么区别?递归下降为什么是现代编译器的首选?AST 如何设计才能既简洁又信息丰富?

一、语法分析的任务与定位#

1.1 从 Token 流到 AST#

flowchart LR TOKENS["Token 流<br/>INT IDENT(x) ASSIGN INT(42) SEMI"] -->|语法分析| AST["AST<br/>VarDecl(int, x, IntLit(42))"] style TOKENS fill:#e3f2fd,stroke:#1565c0 style AST fill:#e8f5e9,stroke:#2e7d32

语法分析器的核心任务:

  1. 结构化:将线性的 Token 流组织为树形结构
  2. 验证:检查 Token 序列是否符合语法规则
  3. 构建 AST:生成抽象语法树,供后续阶段使用
  4. 错误报告:发现语法错误时,给出有意义的错误信息

1.2 解析树 vs AST#

graph TB subgraph 解析树["解析树(Parse Tree)— 保留所有语法细节"] PT1["Decl"] --> PT2["Type"] PT1 --> PT3["Ident"] PT1 --> PT4["="] PT1 --> PT5["Expr"] PT1 --> PT6[";"] PT2 --> PT7["int"] PT3 --> PT8["x"] PT5 --> PT9["42"] end style 解析树 fill:#fff3e0,stroke:#e65100
graph TB subgraph AST["AST(抽象语法树)— 只保留语义信息"] A1["VarDecl"] --> A2["Type: int"] A1 --> A3["name: x"] A1 --> A4["IntLit: 42"] end style AST fill:#e8f5e9,stroke:#2e7d32
特性解析树AST
包含信息所有语法细节(括号、分号等)只有语义相关信息
大小
用途语法分析过程记录后续编译阶段使用
构建者语法分析器自动生成语法分析器手动/自动构建

二、上下文无关文法(CFG)#

2.1 CFG 的定义#

上下文无关文法是描述编程语言语法的数学工具,由四元组 G = (V, T, P, S) 定义:

  • V:非终结符(变量)集合,如 ExprStmt
  • T:终结符(Token)集合,如 INTIDENT+
  • P:产生式规则集合,如 Expr → Expr + Term
  • S:起始符号
# 一个简单的算术表达式文法
# E → E + T | E - T | T
# T → T * F | T / F | F
# F → ( E ) | NUMBER | IDENT
# 用 Python 表示文法
grammar = {
'E': [['E', '+', 'T'], ['E', '-', 'T'], ['T']],
'T': [['T', '*', 'F'], ['T', '/', 'F'], ['F']],
'F': [['(', 'E', ')'], ['NUMBER'], ['IDENT']],
}

2.2 文法的歧义性#

一个文法是歧义的,如果存在一个句子可以推导出两棵不同的解析树:

# 歧义文法:E → E + E | E * E | NUMBER
# 输入:3 + 4 * 5
# 推导 1:(3 + 4) * 5 = 35
# 推导 2:3 + (4 * 5) = 23 ← 数学上正确

消除歧义的方法:

方法说明示例
引入优先级层次不同优先级的运算符用不同的非终结符E → E + T, T → T * F
结合性规则左结合用左递归,右结合用右递归E → E + T(左结合)
优先级声明在文法外声明运算符优先级yacc/bison 的 %left, %right

2.3 左递归与右递归#

# 左递归(适合左结合运算符)
E → E + T | T
# 右递归(适合右结合运算符,如赋值)
E → T + E | T
# 左递归的问题:递归下降分析器会无限递归!
# 解决:消除左递归
# E → T E'
# E' → + T E' | ε
# 消除左递归的算法
def eliminate_left_recursion(grammar):
"""消除文法的直接左递归"""
new_grammar = {}
for A in grammar:
# 分离左递归和非左递归产生式
left_rec = [prod for prod in grammar[A] if prod[0] == A]
non_left_rec = [prod for prod in grammar[A] if prod[0] != A]
if left_rec:
# 创建新的非终结符 A'
A_prime = A + "'"
new_grammar[A] = [prod + [A_prime] for prod in non_left_rec]
new_grammar[A_prime] = [prod[1:] + [A_prime] for prod in left_rec] + [['ε']]
else:
new_grammar[A] = grammar[A]
return new_grammar

三、递归下降分析#

递归下降是最直观的语法分析方法——每个非终结符对应一个函数,函数之间相互递归调用。现代编译器(GCC、Clang、Go、Rust)几乎都使用递归下降。

3.1 基本原理#

flowchart TB subgraph 递归下降分析器 PARSE_E["parse_expr()"] --> PARSE_T["parse_term()"] PARSE_T --> PARSE_F["parse_factor()"] PARSE_F --> PARSE_E PARSE_F --> MATCH_NUM["match(NUMBER)"] PARSE_F --> MATCH_ID["match(IDENT)"] end style 递归下降分析器 fill:#e3f2fd,stroke:#1565c0

3.2 完整的递归下降分析器#

from dataclasses import dataclass
from typing import Union
# AST 节点定义
@dataclass
class NumberLit:
value: int
@dataclass
class Identifier:
name: str
@dataclass
class BinaryOp:
op: str
left: 'Expr'
right: 'Expr'
# 其他节点类型遵循相同模式(UnaryOp、CallExpr、VarDecl、ReturnStmt、Block、FuncDecl 等)
Expr = Union[NumberLit, Identifier, BinaryOp]
class Parser:
def __init__(self, tokens):
self.tokens = tokens
self.pos = 0
def _peek(self):
if self.pos < len(self.tokens):
return self.tokens[self.pos]
return Token(TokenType.EOF, '', 0, 0)
def _advance(self):
token = self._peek()
self.pos += 1
return token
def _expect(self, token_type):
token = self._advance()
if token.type != token_type:
raise SyntaxError(
f"Expected {token_type}, got {token.type} "
f"'{token.value}' at {token.line}:{token.col}"
)
return token
def _match(self, *token_types):
if self._peek().type in token_types:
return self._advance()
return None
# 表达式解析(算符优先)
def parse_expr(self) -> Expr:
return self._parse_comparison()
def _parse_comparison(self) -> Expr:
left = self._parse_addition()
while self._peek().type in (TokenType.LT, TokenType.GT,
TokenType.EQ, TokenType.NEQ):
op = self._advance().value
right = self._parse_addition()
left = BinaryOp(op, left, right)
return left
def _parse_addition(self) -> Expr:
left = self._parse_multiplication()
while self._peek().type in (TokenType.PLUS, TokenType.MINUS):
op = self._advance().value
right = self._parse_multiplication()
left = BinaryOp(op, left, right)
return left
def _parse_multiplication(self) -> Expr:
left = self._parse_unary()
while self._peek().type in (TokenType.STAR, TokenType.SLASH):
op = self._advance().value
right = self._parse_unary()
left = BinaryOp(op, left, right)
return left
def _parse_unary(self) -> Expr:
if self._peek().type == TokenType.MINUS:
self._advance()
operand = self._parse_unary()
return UnaryOp('-', operand)
return self._parse_primary()
def _parse_primary(self) -> Expr:
token = self._peek()
if token.type == TokenType.INTEGER:
self._advance()
return NumberLit(int(token.value))
elif token.type == TokenType.IDENTIFIER:
self._advance()
if self._peek().type == TokenType.LPAREN:
return self._parse_call(token.value)
return Identifier(token.value)
elif token.type == TokenType.LPAREN:
self._advance() # (
expr = self.parse_expr()
self._expect(TokenType.RPAREN) # )
return expr
else:
raise SyntaxError(
f"Unexpected token {token.type} '{token.value}' "
f"at {token.line}:{token.col}"
)
def _parse_call(self, func_name: str) -> CallExpr:
self._expect(TokenType.LPAREN)
args = []
if self._peek().type != TokenType.RPAREN:
args.append(self.parse_expr())
while self._match(TokenType.COMMA):
args.append(self.parse_expr())
self._expect(TokenType.RPAREN)
return CallExpr(func_name, args)

3.3 递归下降的优缺点#

优点缺点
代码直观,易于理解和维护左递归文法需要改写
错误信息精确,可自定义需要手动处理优先级和结合性
可以在解析过程中构建 AST回溯可能导致性能问题
支持任意上下文相关语法不如 LR 分析器严格
几乎所有现代编译器使用需要手动提取左公因子

四、LL 分析与 LR 分析#

4.1 分析策略的分类#

graph TB PARSER["语法分析器"] PARSER --> TD["自顶向下<br/>Top-Down"] PARSER --> BU["自底向上<br/>Bottom-Up"] TD --> LL1["LL(1)<br/>递归下降"] TD --> LLK["LL(k)<br/>前瞻 k 个 Token"] TD --> PRED["预测分析<br/>表驱动"] BU --> SLR["SLR(1)<br/>简单 LR"] BU --> LALR["LALR(1)<br/>前瞻 LR"] BU --> CLR["CLR(1)<br/>规范 LR"] style PARSER fill:#e3f2fd,stroke:#1565c0 style TD fill:#e8f5e9,stroke:#2e7d32 style BU fill:#fff3e0,stroke:#e65100

4.2 LL 分析 vs LR 分析#

特性LL 分析LR 分析
方向自顶向下自底向上
推导最左推导最右推导的逆过程
构建顺序从根到叶从叶到根
文法限制不能有左递归允许左递归
文法范围LL 文法 ⊂ LR 文法更大的文法类
实现方式递归下降或预测表移进-归约自动机
错误检测较早较晚
典型工具递归下降、ANTLRyacc/bison、LALR
典型用户GCC、Clang、Go、RustGCC(旧版)、Python

4.3 LL(1) 分析#

LL(1) 分析使用一个前瞻 Token决定选择哪个产生式:

# LL(1) 预测分析表
# 文法:
# E → T E'
# E' → + T E' | ε
# T → F T'
# T' → * F T' | ε
# F → ( E ) | NUMBER
predict_table = {
('E', '('): ['T', "E'"],
('E', 'NUMBER'): ['T', "E'"],
("E'", '+'): ['+', 'T', "E'"],
("E'", ')'): ['ε'],
("E'", '$'): ['ε'],
('T', '('): ['F', "T'"],
('T', 'NUMBER'): ['F', "T'"],
("T'", '*'): ['*', 'F', "T'"],
("T'", '+'): ['ε'],
("T'", ')'): ['ε'],
("T'", '$'): ['ε'],
('F', '('): ['(', 'E', ')'],
('F', 'NUMBER'): ['NUMBER'],
}

4.4 LR 分析:移进-归约#

LR 分析使用状态机,核心操作是移进(Shift)和归约(Reduce):

flowchart TB subgraph LR分析过程 S1["状态 0<br/>栈: 0"] -->|移进 NUMBER| S2["状态 5<br/>栈: 0 NUMBER 5"] S2 -->|归约 F→NUMBER| S3["状态 3<br/>栈: 0 F 3"] S3 -->|归约 T→F| S4["状态 2<br/>栈: 0 T 2"] S4 -->|归约 E→T| S5["状态 1<br/>栈: 0 E 1"] end style S1 fill:#e3f2fd,stroke:#1565c0 style S5 fill:#e8f5e9,stroke:#2e7d32
# 简化的 LR 分析器
def lr_parse(tokens, action_table, goto_table):
stack = [0] # 状态栈
pos = 0
while True:
state = stack[-1]
token = tokens[pos]
action = action_table.get((state, token.type))
if action is None:
raise SyntaxError(f"Syntax error at {token}")
if action[0] == 'shift':
stack.append(token)
stack.append(action[1]) # 新状态
pos += 1
elif action[0] == 'reduce':
prod_len = action[2] # 产生式右部长度
non_terminal = action[1] # 产生式左部
# 弹出 2*prod_len 个元素
for _ in range(2 * prod_len):
stack.pop()
state = stack[-1]
stack.append(non_terminal)
stack.append(goto_table[(state, non_terminal)])
elif action[0] == 'accept':
return "Parse successful!"

4.5 SLR vs LALR vs CLR 对比#

特性SLR(1)LALR(1)CLR(1)
全称Simple LRLook-Ahead LRCanonical LR
状态数少(与 SLR 相同)多(可能很大)
文法范围最小中等最大
实现复杂度简单中等复杂
冲突可能性
典型工具yacc/bison
实际使用教学用最广泛理论研究
Note

LALR(1) 是实际使用最广泛的 LR 分析方法。yacc/bison 默认生成 LALR(1) 分析器。LALR(1) 的状态数与 SLR 相同,但通过更精确的 lookahead 集合减少了冲突。

五、算符优先分析#

算符优先分析是一种简化版的 LR 分析,专门用于处理表达式:

5.1 优先级表#

# 运算符优先级表
precedence = {
('+', '+'): '>', ('+', '-'): '>', ('+', '*'): '<', ('+', '/'): '<',
('-', '+'): '>', ('-', '-'): '>', ('-', '*'): '<', ('-', '/'): '<',
('*', '+'): '>', ('*', '-'): '>', ('*', '*'): '>', ('*', '/'): '>',
('/', '+'): '>', ('/', '-'): '>', ('/', '*'): '>', ('/', '/'): '>',
# '$' 是结束标记
('$', '+'): '<', ('$', '*'): '<', ('+', '$'): '>', ('*', '$'): '>',
}
# '<' : 栈顶运算符优先级低,移进
# '>' : 栈顶运算符优先级高,归约
# '=' : 优先级相同(用于括号匹配)

5.2 Pratt 解析器#

Pratt 解析器是递归下降的一种变体,通过绑定力(binding power)优雅地处理优先级:

class PrattParser:
def __init__(self, tokens):
self.tokens = tokens
self.pos = 0
# 绑定力度表:值越大优先级越高
PREFIX_BP = {
'-': 7, '!': 7,
}
INFIX_BP = {
'=': (2, 1), # 右结合:左绑定力 2,右绑定力 1
'+': (5, 6), # 左结合:左绑定力 5,右绑定力 6
'-': (5, 6),
'*': (7, 8),
'/': (7, 8),
}
def parse_expr(self, min_bp=0):
# 前缀操作
left = self.parse_prefix()
# 中缀操作
while True:
op = self._peek().value
if op not in self.INFIX_BP:
break
(left_bp, right_bp) = self.INFIX_BP[op]
if left_bp < min_bp:
break
self._advance()
right = self.parse_expr(right_bp)
left = BinaryOp(op, left, right)
return left
def parse_prefix(self):
token = self._advance()
if token.type == TokenType.INTEGER:
return NumberLit(int(token.value))
elif token.type == TokenType.IDENTIFIER:
return Identifier(token.value)
elif token.value in self.PREFIX_BP:
bp = self.PREFIX_BP[token.value]
operand = self.parse_expr(bp)
return UnaryOp(token.value, operand)
elif token.type == TokenType.LPAREN:
expr = self.parse_expr(0)
self._expect(TokenType.RPAREN)
return expr
else:
raise SyntaxError(f"Unexpected token: {token}")

六、AST 设计#

6.1 AST 节点类型体系#

graph TB NODE["AST Node"] NODE --> EXPR["Expr"] NODE --> STMT["Stmt"] NODE --> DECL["Decl"] EXPR --> BINARY["BinaryOp<br/>a + b"] EXPR --> UNARY["UnaryOp<br/>-x"] EXPR --> LIT["Literal<br/>42, 'hello'"] EXPR --> ID["Identifier<br/>x"] EXPR --> CALL["CallExpr<br/>f(x)"] EXPR --> INDEX["IndexExpr<br/>a[i]"] STMT --> EXPRSTMT["ExprStmt<br/>f();"] STMT --> IF["IfStmt<br/>if-else"] STMT --> WHILE["WhileStmt<br/>while"] STMT --> RETURN["ReturnStmt<br/>return"] STMT --> BLOCK["Block<br/>{...}"] DECL --> VARDECL["VarDecl<br/>int x = 0"] DECL --> FUNCDECL["FuncDecl<br/>int f()"] style NODE fill:#e3f2fd,stroke:#1565c0 style EXPR fill:#e8f5e9,stroke:#2e7d32 style STMT fill:#fff3e0,stroke:#e65100 style DECL fill:#fce4ec,stroke:#c62828

6.2 AST 的访问者模式#

class ASTVisitor:
"""AST 访问者基类"""
def visit(self, node):
method_name = f'visit_{type(node).__name__}'
method = getattr(self, method_name, self.generic_visit)
return method(node)
def generic_visit(self, node):
raise NotImplementedError(
f"No visit_{type(node).__name__} method"
)
class PrettyPrinter(ASTVisitor):
"""AST 美化打印"""
def visit_BinaryOp(self, node):
left = self.visit(node.left)
right = self.visit(node.right)
return f"({left} {node.op} {right})"
def visit_NumberLit(self, node):
return str(node.value)
def visit_Identifier(self, node):
return node.name
def visit_UnaryOp(self, node):
operand = self.visit(node.operand)
return f"({node.op}{operand})"
def visit_VarDecl(self, node):
init = f" = {self.visit(node.init)}" if node.init else ""
return f"{node.type_name} {node.name}{init};"
# 使用
ast = VarDecl("int", "x", BinaryOp("+", NumberLit(3), NumberLit(4)))
printer = PrettyPrinter()
print(printer.visit(ast)) # int x = (3 + 4);

6.3 不同语言的 AST 对比#

语言AST 特点特殊节点
C简洁,与语法结构对应预处理器指令、逗号表达式
Java丰富,包含类型信息注解、泛型、Lambda
Python缩进敏感Suite(缩进块)、ListComp
Rust类型丰富生命周期标注、模式匹配
Go简洁多赋值、defer、go

七、错误恢复#

7.1 语法错误的类型#

错误类型示例说明
缺少 Tokenint x = 42(缺少分号)最常见的错误
多余 Tokenint x = = 42;多了一个等号
Token 顺序错误x int = 42;类型和名称反了
结构不匹配{ if (x) }if 缺少语句体

7.2 错误恢复策略#

flowchart TB ERROR["发现语法错误"] --> STRATEGY["选择恢复策略"] STRATEGY --> PANIC["恐慌模式<br/>跳到同步 Token"] STRATEGY --> PHRASE["短语级恢复<br/>局部修正"] STRATEGY --> GLOBAL["全局纠正<br/>最小编辑距离"] style ERROR fill:#fce4ec,stroke:#c62828 style PANIC fill:#e8f5e9,stroke:#2e7d32 style PHRASE fill:#fff3e0,stroke:#e65100 style GLOBAL fill:#e3f2fd,stroke:#1565c0

7.3 恐慌模式恢复#

恐慌模式是最常用的错误恢复策略——当发现错误时,跳过 Token 直到遇到同步 Token(通常是语句结束符 ; 或块结束符 }):

class ParserWithErrorRecovery(Parser):
# 同步 Token 集合
SYNC_TOKENS = {TokenType.SEMI, TokenType.RBRACE, TokenType.EOF}
def _synchronize(self):
"""跳过 Token 直到遇到同步点"""
while self._peek().type not in self.SYNC_TOKENS:
self._advance()
# 跳过同步 Token 本身
if self._peek().type == TokenType.SEMI:
self._advance()
def parse_stmt(self):
try:
return super().parse_stmt()
except SyntaxError as e:
self.errors.append(e)
self._synchronize()
return None # 返回空节点
Warning

错误恢复是一个困难的问题——恢复后可能产生”级联错误”(一个错误导致后续多个虚假错误)。好的错误恢复应该尽量减少级联错误,并在第一个错误后继续报告有意义的错误。

八、语法分析工具#

8.1 主流语法分析工具对比#

工具分析方法输入语言输出语言特点
yacc/bisonLALR(1)CC经典工具,POSIX 标准
ANTLRALL(*)多语言Java/Python/C++/Go自适应 LL,强大
LarkEarley/LALRPythonPython简洁易用
Tree-sitterGLRCC增量解析,编辑器集成
pestPEGRustRustRust 生态首选
nom组合子RustRust函数式风格

8.2 ANTLR 使用示例#

# 安装 ANTLR
pip install antlr4-python3-runtime
# 创建语法文件
cat > Expr.g4 << 'EOF'
grammar Expr;
expr: expr ('*' | '/') expr
| expr ('+' | '-') expr
| NUMBER
| IDENT
| '(' expr ')'
;
NUMBER: [0-9]+;
IDENT: [a-zA-Z_][a-zA-Z0-9_]*;
WS: [ \t\r\n]+ -> skip;
EOF
# 生成 Python 解析器
antlr4 -Dlanguage=Python3 Expr.g4

九、动手实践#

9.1 实验一:手写递归下降分析器#

# 使用上面的 Parser 类
from lexer import Lexer, TokenType
source = "int x = 3 + 4 * 5;"
lexer = Lexer(source)
tokens = lexer.tokenize()
parser = Parser(tokens)
ast = parser.parse_var_decl()
print(ast) # VarDecl(type_name='int', name='x', init=BinaryOp(op='+', ...))

9.2 实验二:用 Clang 查看 AST#

# 查看 Clang 的 AST 输出
cat > test.c << 'EOF'
int add(int a, int b) {
return a + b;
}
EOF
clang -ast-dump test.c 2>/dev/null

9.3 实验三:可视化 AST#

import graphviz
def visualize_ast(node, dot=None, counter=[0]):
if dot is None:
dot = graphviz.Digraph()
dot.attr(rankdir='TB')
node_id = f"n{counter[0]}"
counter[0] += 1
if isinstance(node, BinaryOp):
dot.node(node_id, f"BinaryOp\\n{node.op}")
left_id = visualize_ast(node.left, dot, counter)
right_id = visualize_ast(node.right, dot, counter)
dot.edge(node_id, left_id)
dot.edge(node_id, right_id)
elif isinstance(node, NumberLit):
dot.node(node_id, f"NumberLit\\n{node.value}")
elif isinstance(node, Identifier):
dot.node(node_id, f"Identifier\\n{node.name}")
return node_id
# 使用
ast = BinaryOp('+', BinaryOp('*', NumberLit(4), NumberLit(5)), NumberLit(3))
dot = visualize_ast(ast)
dot.render('ast', format='png', cleanup=True)

十、本章小结#

上一章中,我们看到字符流如何通过正则表达式、NFA、DFA 的转换链被分解为结构化的 Token 流。但 Token 流仍然是线性的——它只告诉编译器”有哪些词”,却不告诉编译器”这些词如何组成句子”。要理解 Token 之间的结构关系,就需要语法分析将它们组织为抽象语法树。

概念要点
语法分析任务Token 流 → AST,结构化、验证、构建、报告错误
CFG上下文无关文法,描述语言语法的数学工具
歧义性同一句子多棵解析树,需通过优先级和结合性消除
递归下降每个非终结符一个函数,直观、灵活、现代编译器首选
LL 分析自顶向下,需消除左递归,LL(1) 最常用
LR 分析自底向上,移进-归约,LALR(1) 最常用
Pratt 解析绑定力度处理优先级,优雅高效
AST 设计节点类型体系 + 访问者模式
错误恢复恐慌模式最常用,注意级联错误

以上就是语法分析的核心内容。下一章进入 语义分析,看看 AST 如何获得类型信息和语义含义。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

语法分析:从 Token 到 AST
https://blog.souloss.com/posts/compiler/syntax-analysis/
作者
Souloss
发布于
2025-12-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时