03-语法分析与AST¶
重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐ 学习时间:2天 必须掌握:是
为什么学这一章?¶
语法分析是编译器前端的核心环节。它将词法分析输出的Token流按照语法规则组织成抽象语法树(AST),为后续的语义分析和代码生成提供结构化表示。
学完这一章,你将能够: - ✅ 理解上下文无关文法(CFG)和BNF表示法 - ✅ 掌握递归下降分析方法 - ✅ 手写一个简单的语法分析器 - ✅ 理解AST的结构与用途
📖 核心概念¶
1. 上下文无关文法(CFG)¶
语法分析的理论基础是上下文无关文法,用产生式规则描述语言结构:
┌─────────────────────────────────────────────────────────────┐
│ 简单算术表达式的文法 │
├─────────────────────────────────────────────────────────────┤
│ │
│ BNF(巴科斯-诺尔范式)描述: │
│ │
│ <expression> ::= <term> (('+' | '-') <term>)* │
│ <term> ::= <factor> (('*' | '/') <factor>)* │
│ <factor> ::= NUMBER | '(' <expression> ')' | IDENT │
│ │
│ 示例推导:3 + 4 * 2 │
│ expression │
│ └── term(3) + term(4*2) │
│ ├── factor(3) │
│ └── factor(4) * factor(2) │
│ │
│ 优先级通过文法层次隐式表达: │
│ expression → 加减(低优先级) │
│ term → 乘除(高优先级) │
│ factor → 原子值(最高优先级) │
│ │
└─────────────────────────────────────────────────────────────┘
2. 抽象语法树(AST)¶
AST是源代码的树形中间表示,去除了无关的语法细节(如括号、分号):
┌─────────────────────────────────────────────────────────────┐
│ 表达式 3 + 4 * 2 的AST │
├─────────────────────────────────────────────────────────────┤
│ │
│ [+] ← 根节点:加法 │
│ / \ │
│ [3] [*] ← 右子树:乘法(优先级更高) │
│ / \ │
│ [4] [2] │
│ │
│ 注意:AST正确反映了运算优先级 │
│ 先计算 4*2=8,再计算 3+8=11 │
│ │
└─────────────────────────────────────────────────────────────┘
3. 语法分析方法分类¶
┌─────────────────────────────────────────────────────────────┐
│ 语法分析方法 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 自顶向下分析(Top-Down) │
│ ├── 递归下降分析 ← 手写常用,简单直观 │
│ ├── LL(1)分析 ← 预测分析表驱动 │
│ └── LL(k)分析 ← 需要k个前瞻Token │
│ │
│ 自底向上分析(Bottom-Up) │
│ ├── LR(0)分析 ← 能力有限 │
│ ├── SLR(1)分析 ← 简单LR │
│ ├── LALR(1)分析 ← Yacc/Bison使用的方法 │
│ └── LR(1)分析 ← 最强大但状态多 │
│ │
│ 实际选择: │
│ • 手写编译器 → 递归下降(GCC、Clang等均使用) │
│ • 生成工具 → LALR(1)(Yacc/Bison) │
│ │
└─────────────────────────────────────────────────────────────┘
4. 手写递归下降语法分析器¶
以下用C语言实现完整的表达式解析器,构建AST并求值:
// parser.c - 递归下降语法分析器
#include <stdio.h> // 引入头文件
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// ========== AST节点定义 ==========
typedef enum {
NODE_NUMBER, // 数字字面量
NODE_BINOP, // 二元运算
NODE_UNARY // 一元运算(负号)
} NodeType;
typedef struct ASTNode { // struct结构体:自定义复合数据类型
NodeType type;
union {
int number; // NODE_NUMBER
struct { // NODE_BINOP
char op;
struct ASTNode *left;
struct ASTNode *right;
} binop;
struct { // NODE_UNARY
char op;
struct ASTNode *operand;
} unary;
};
} ASTNode;
// 创建数字节点
ASTNode *make_number(int value) {
ASTNode *node = (ASTNode *)malloc(sizeof(ASTNode)); // 动态内存分配,需手动free释放
node->type = NODE_NUMBER;
node->number = value;
return node;
}
// 创建二元运算节点
ASTNode *make_binop(char op, ASTNode *left, ASTNode *right) {
ASTNode *node = (ASTNode *)malloc(sizeof(ASTNode));
node->type = NODE_BINOP;
node->binop.op = op;
node->binop.left = left;
node->binop.right = right;
return node;
}
// ========== 简易Lexer ==========
typedef enum { T_NUM, T_PLUS, T_MINUS, T_MUL, T_DIV, T_LPAREN, T_RPAREN, T_EOF } TokType;
typedef struct { TokType type; int value; } Token;
const char *src; // 指针:存储变量的内存地址
int pos;
Token next_token() {
while (src[pos] == ' ') pos++;
Token t;
if (src[pos] == '\0') { t.type = T_EOF; return t; }
if (isdigit(src[pos])) {
t.type = T_NUM;
t.value = 0;
while (isdigit(src[pos])) { t.value = t.value * 10 + (src[pos++] - '0'); }
return t;
}
char c = src[pos++];
switch (c) {
case '+': t.type = T_PLUS; break;
case '-': t.type = T_MINUS; break;
case '*': t.type = T_MUL; break;
case '/': t.type = T_DIV; break;
case '(': t.type = T_LPAREN; break;
case ')': t.type = T_RPAREN; break;
default:
fprintf(stderr, "未知字符: '%c'\n", c);
exit(1);
}
return t;
}
// ========== 递归下降解析 ==========
Token current_token;
void eat(TokType expected) {
if (current_token.type != expected) {
fprintf(stderr, "语法错误: 期望Token类型 %d, 实际 %d\n", expected, current_token.type);
exit(1);
}
current_token = next_token();
}
// 前向声明
ASTNode *parse_expression();
// factor = NUMBER | '(' expression ')'
ASTNode *parse_factor() {
if (current_token.type == T_NUM) {
int val = current_token.value;
eat(T_NUM);
return make_number(val);
}
if (current_token.type == T_LPAREN) {
eat(T_LPAREN);
ASTNode *node = parse_expression();
eat(T_RPAREN);
return node;
}
fprintf(stderr, "语法错误: 期望数字或左括号\n");
exit(1);
}
// term = factor (('*' | '/') factor)*
ASTNode *parse_term() {
ASTNode *node = parse_factor();
while (current_token.type == T_MUL || current_token.type == T_DIV) {
char op = (current_token.type == T_MUL) ? '*' : '/';
current_token = next_token();
ASTNode *right = parse_factor();
node = make_binop(op, node, right);
}
return node;
}
// expression = term (('+' | '-') term)*
ASTNode *parse_expression() {
ASTNode *node = parse_term();
while (current_token.type == T_PLUS || current_token.type == T_MINUS) {
char op = (current_token.type == T_PLUS) ? '+' : '-';
current_token = next_token();
ASTNode *right = parse_term();
node = make_binop(op, node, right);
}
return node;
}
// ========== AST求值 ==========
int eval(ASTNode *node) {
if (node->type == NODE_NUMBER) return node->number;
if (node->type == NODE_BINOP) {
int l = eval(node->binop.left);
int r = eval(node->binop.right);
switch (node->binop.op) {
case '+': return l + r;
case '-': return l - r;
case '*': return l * r;
case '/': return r != 0 ? l / r : 0;
}
}
return 0;
}
// ========== AST打印 ==========
void print_ast(ASTNode *node, int depth) {
for (int i = 0; i < depth; i++) printf(" ");
if (node->type == NODE_NUMBER) {
printf("Number(%d)\n", node->number);
} else if (node->type == NODE_BINOP) {
printf("BinOp('%c')\n", node->binop.op);
print_ast(node->binop.left, depth + 1);
print_ast(node->binop.right, depth + 1);
}
}
// 释放AST
void free_ast(ASTNode *node) {
if (!node) return;
if (node->type == NODE_BINOP) {
free_ast(node->binop.left);
free_ast(node->binop.right);
}
free(node);
}
int main() {
const char *expressions[] = {
"3 + 4 * 2",
"(3 + 4) * 2",
"10 - 2 * 3 + 1",
"100 / (5 * 4)",
};
int n = sizeof(expressions) / sizeof(expressions[0]);
for (int i = 0; i < n; i++) {
src = expressions[i];
pos = 0;
current_token = next_token();
printf("表达式: %s\n", src);
ASTNode *ast = parse_expression();
printf("AST结构:\n");
print_ast(ast, 1);
printf("计算结果: %d\n\n", eval(ast));
free_ast(ast);
}
return 0;
}
🔍 深入理解¶
LL(1) 与 FIRST/FOLLOW 集合¶
LL(1)分析需要为每个产生式计算FIRST和FOLLOW集合:
以文法 E → T E' E' → + T E' | ε T → F T'
T' → * F T' | ε F → ( E ) | id
FIRST(E) = FIRST(T) = FIRST(F) = { (, id }
FIRST(E') = { +, ε }
FIRST(T') = { *, ε }
FOLLOW(E) = { ), $ }
FOLLOW(E') = { ), $ }
FOLLOW(T) = { +, ), $ }
FOLLOW(T') = { +, ), $ }
处理二义性文法¶
经典的"悬空else"问题:
// 这段代码的else属于哪个if?
if (a)
if (b)
stmt1;
else // 属于内层if(b)还是外层if(a)?
stmt2;
// C/C++规则:else与最近的未匹配if配对
// 即 else 属于 if(b)
💡 面试常见问题¶
Q1:什么是左递归?如何消除?¶
答:左递归是指产生式形如 A → Aα | β,递归下降分析器会无限递归。消除方法是改写为右递归:A → βA',A' → αA' | ε。例如 E → E+T | T 改为 E → TE',E' → +TE' | ε。
Q2:AST和解析树(Parse Tree)有什么区别?¶
答:解析树保留所有语法细节(括号、分号等),是文法推导的完整记录;AST只保留语义相关信息,省略冗余语法节点。AST更紧凑,是后续编译阶段的标准输入。
Q3:为什么GCC和Clang选择手写递归下降而不用Yacc?¶
答:①手写Parser可提供更好的错误消息;②对语法规则有完全控制权,便于处理C++复杂语法;③性能可以更优;④易于维护和扩展。现代编译器大多采用手写方式。
Q4:什么是运算符优先级爬升法(Pratt Parsing)?¶
答:Pratt Parsing也叫优先级爬升法,通过给运算符赋予优先级数值,用一个循环代替多层递归函数。代码更简洁,容易添加新运算符。许多语言的表达式解析器采用此方法。
Q5:LL和LR分析的主要区别?¶
答:LL是自顶向下,从左读入,最左推导,适合手写(如递归下降);LR是自底向上,从左读入,最右推导的逆过程,能力更强,适合工具生成(Yacc/Bison)。LR能处理所有LL文法,但反之不然。
📝 本章小结¶
┌─────────────────────────────────────────────┐
│ 本章核心知识点 │
├─────────────────────────────────────────────┤
│ │
│ 1. CFG用产生式描述语言的语法结构 │
│ 2. AST是源代码的树形中间表示 │
│ 3. 递归下降是最常用的手写分析方法 │
│ 4. 运算符优先级通过文法层次表达 │
│ 5. LL自顶向下 vs LR自底向上 │
│ 6. 错误恢复是语法分析的重要组成部分 │
│ │
└─────────────────────────────────────────────┘
下一章:04-中间代码与优化 - 从AST到中间表示,以及编译器优化