第71集:语法分析器是什么?

学习目标

  • 理解语法分析的核心任务
  • 掌握语法分析器在编译过程中的位置和作用
  • 了解语法错误的概念和处理方式
  • 认识语法树的结构和作用

一、语法分析的任务

语法分析是编译过程的第二个阶段,紧随词法分析之后。如果说词法分析是将源代码分解为一个个 Token(词法单元),那么语法分析就是将这些 Token 按照语言的语法规则组织成有意义的结构。

核心任务

  1. 结构识别:分析 Token 序列是否符合语言的语法规则
  2. 语法树构建:将符合语法规则的 Token 序列构建成语法树(Syntax Tree)
  3. 错误检测:当发现语法错误时,及时报告错误位置和类型
  4. 错误恢复:在遇到错误时,尝试恢复分析过程,以便发现更多错误

输入与输出

  • 输入:词法分析器生成的 Token 序列
  • 输出:语法树(或抽象语法树 AST)

二、语法分析器的位置

在整个编译过程中,语法分析器位于词法分析器和语义分析器之间,起到承上启下的作用:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ 词法分析器  │────>│ 语法分析器  │────>│ 语义分析器  │────>│ 中间代码生成 │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
     ↑                   ↑                   ↑                   ↑
     │                   │                   │                   │
  字符流            Token 流             语法树              中间代码

与其他组件的关系

  • 与词法分析器:语法分析器从词法分析器获取 Token,是词法分析的直接消费者
  • 与语义分析器:语法分析器生成的语法树是语义分析的输入
  • 与错误处理:语法分析器需要检测和报告语法错误,为后续处理提供错误信息

三、语法错误

在编译过程中,语法错误是常见的问题,语法分析器需要能够有效地检测和处理这些错误。

常见的语法错误

  1. 缺少符号:例如缺少分号、括号等

    int main() {
        printf("Hello")  // 缺少分号
    }
  2. 多余符号:例如多余的分号、括号等

    int main() {
        int a = 10;;  // 多余的分号
    }
  3. 符号顺序错误:例如运算符顺序错误

    int main() {
        int a = 10 = b;  // 赋值运算符顺序错误
    }
  4. 结构不完整:例如缺少函数体、循环体等

    int main()
        // 缺少函数体

错误处理策略

  1. 错误检测:识别不符合语法规则的 Token 序列
  2. 错误报告:向用户报告错误的位置、类型和可能的原因
  3. 错误恢复:尝试从错误中恢复,继续分析后续代码
  4. 错误计数:统计错误数量,决定是否继续编译

四、语法树的作用

语法树是语法分析器的核心输出,它是源代码的结构化表示,具有重要的作用。

语法树的结构

语法树以树状结构表示源代码的语法结构,其中:

  • 根节点:表示整个程序或主要结构
  • 内部节点:表示运算符、语句、表达式等
  • 叶节点:表示终结符(如标识符、常量、运算符等)

例如,表达式 a + b * c 的语法树:

    +
   / \
  a   *
     / \
    b   c

语法树的作用

  1. 结构表示:清晰地表示源代码的语法结构
  2. 语义分析基础:为语义分析提供结构化的输入
  3. 代码生成基础:为后续的代码生成提供基础
  4. 优化基础:为代码优化提供分析对象
  5. 错误定位:帮助精确定位语法错误的位置

抽象语法树(AST)

在实际编译器中,通常使用抽象语法树(Abstract Syntax Tree, AST),它是语法树的简化版本,去除了一些语法细节,更加专注于语义结构。

例如,表达式 a + b * c 的抽象语法树:

    Add
   /   \
  a     Mul
       /   \
      b     c

五、语法分析器的类型

根据分析方法的不同,语法分析器可以分为两大类:

1. 自顶向下分析(Top-Down Parsing)

  • 基本思想:从语法的开始符号出发,逐步推导产生与输入 Token 序列匹配的结构
  • 代表算法
    • 递归下降分析(Recursive Descent Parsing)
    • LL(1) 分析
  • 特点:实现简单,易于理解,适合手写

2. 自底向上分析(Bottom-Up Parsing)

  • 基本思想:从输入 Token 序列出发,逐步归约为语法的开始符号
  • 代表算法
    • 移进-归约分析(Shift-Reduce Parsing)
    • LR 分析(包括 LR(0)、SLR(1)、LR(1)、LALR(1))
  • 特点:功能强大,能处理更多类型的文法,适合自动生成

六、代码示例:简单的语法分析器框架

下面是一个使用递归下降法实现的简单语法分析器框架,用于分析简单的算术表达式:

class Parser:
    def __init__(self, tokens):
        self.tokens = tokens
        self.pos = 0
    
    def current_token(self):
        if self.pos < len(self.tokens):
            return self.tokens[self.pos]
        return None
    
    def eat(self, token_type):
        if self.current_token() and self.current_token()[0] == token_type:
            self.pos += 1
            return True
        return False
    
    def factor(self):
        """分析因子:数字或括号包围的表达式"""
        token = self.current_token()
        if token and token[0] == 'NUMBER':
            self.eat('NUMBER')
            return {'type': 'NUMBER', 'value': token[1]}
        elif token and token[0] == 'LPAREN':
            self.eat('LPAREN')
            node = self.expr()
            if self.eat('RPAREN'):
                return node
            else:
                raise SyntaxError("Missing closing parenthesis")
        else:
            raise SyntaxError(f"Unexpected token: {token}")
    
    def term(self):
        """分析项:因子的乘除运算"""
        node = self.factor()
        while self.current_token() and self.current_token()[0] in ['MUL', 'DIV']:
            token = self.current_token()
            if token[0] == 'MUL':
                self.eat('MUL')
                node = {'type': 'MUL', 'left': node, 'right': self.factor()}
            elif token[0] == 'DIV':
                self.eat('DIV')
                node = {'type': 'DIV', 'left': node, 'right': self.factor()}
        return node
    
    def expr(self):
        """分析表达式:项的加减运算"""
        node = self.term()
        while self.current_token() and self.current_token()[0] in ['ADD', 'SUB']:
            token = self.current_token()
            if token[0] == 'ADD':
                self.eat('ADD')
                node = {'type': 'ADD', 'left': node, 'right': self.term()}
            elif token[0] == 'SUB':
                self.eat('SUB')
                node = {'type': 'SUB', 'left': node, 'right': self.term()}
        return node
    
    def parse(self):
        """开始分析"""
        return self.expr()

# 测试
if __name__ == "__main__":
    # 假设我们有一个 Token 序列
    tokens = [
        ('NUMBER', 1),
        ('ADD', '+'),
        ('NUMBER', 2),
        ('MUL', '*'),
        ('NUMBER', 3)
    ]
    
    parser = Parser(tokens)
    try:
        ast = parser.parse()
        print("解析成功!")
        print(ast)
    except SyntaxError as e:
        print(f"语法错误:{e}")

七、自测题

  1. 语法分析器的主要任务是什么?
  2. 语法分析器在编译过程中的位置是什么?
  3. 什么是语法错误?请举例说明常见的语法错误。
  4. 语法树的作用是什么?
  5. 自顶向下分析和自底向上分析的区别是什么?

八、总结与展望

本集介绍了语法分析器的基本概念、任务、位置、语法错误处理以及语法树的作用。语法分析是编译过程中的重要环节,它将词法分析器生成的 Token 序列组织成有意义的结构,为后续的语义分析和代码生成奠定基础。

在接下来的几集中,我们将深入学习语法分析的具体方法和技术,包括:

  • 上下文无关文法回顾
  • 消除左递归
  • 提取左公因子
  • 递归下降分析器实现
  • LL(1) 分析表构造

通过这些学习,我们将能够理解和实现各种语法分析器,为构建完整的编译器打下坚实的基础。

九、参考资料

  1. 《编译原理》(龙书)- Alfred V. Aho 等
  2. 《现代编译原理》(虎书)- Andrew W. Appel
  3. 《编译器设计》- Keith D. Cooper 等
  4. LLVM 项目文档
« 上一篇 词法分析篇总结 下一篇 » 上下文无关文法回顾