跳转至

03-语法分析与AST

重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐ 学习时间:2天 必须掌握:是


为什么学这一章?

语法分析是编译器前端的核心环节。它将词法分析输出的Token流按照语法规则组织成抽象语法树(AST),为后续的语义分析和代码生成提供结构化表示。

学完这一章,你将能够: - ✅ 理解上下文无关文法(CFG)和BNF表示法 - ✅ 掌握递归下降分析方法 - ✅ 手写一个简单的语法分析器 - ✅ 理解AST的结构与用途


📖 核心概念

1. 上下文无关文法(CFG)

语法分析的理论基础是上下文无关文法,用产生式规则描述语言结构:

Text Only
┌─────────────────────────────────────────────────────────────┐
│              简单算术表达式的文法                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  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是源代码的树形中间表示,去除了无关的语法细节(如括号、分号):

Text Only
┌─────────────────────────────────────────────────────────────┐
│        表达式  3 + 4 * 2  的AST                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│              [+]              ← 根节点:加法                 │
│             /   \                                           │
│           [3]   [*]           ← 右子树:乘法(优先级更高)   │
│                /   \                                        │
│              [4]   [2]                                      │
│                                                             │
│  注意:AST正确反映了运算优先级                               │
│  先计算 4*2=8,再计算 3+8=11                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3. 语法分析方法分类

Text Only
┌─────────────────────────────────────────────────────────────┐
│                  语法分析方法                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  自顶向下分析(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并求值:

C
// 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;
}
Bash
gcc parser.c -o parser && ./parser  # &&前一个成功才执行后一个;||前一个失败才执行

🔍 深入理解

LL(1) 与 FIRST/FOLLOW 集合

LL(1)分析需要为每个产生式计算FIRST和FOLLOW集合:

Text Only
以文法 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"问题:

C
// 这段代码的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文法,但反之不然。


📝 本章小结

Text Only
┌─────────────────────────────────────────────┐
│              本章核心知识点                    │
├─────────────────────────────────────────────┤
│                                             │
│  1. CFG用产生式描述语言的语法结构            │
│  2. AST是源代码的树形中间表示               │
│  3. 递归下降是最常用的手写分析方法          │
│  4. 运算符优先级通过文法层次表达            │
│  5. LL自顶向下 vs LR自底向上               │
│  6. 错误恢复是语法分析的重要组成部分        │
│                                             │
└─────────────────────────────────────────────┘

下一章04-中间代码与优化 - 从AST到中间表示,以及编译器优化