递归下降、LL(1)、LR(1) - 动态可视化讲解
输入:Token 序列(词法分析的输出)
输出:语法树(Parse Tree 或 AST)
任务:
1. 根据 文法(Grammar) 检查 Token 序列是否符合语法规则
2. 如果符合,构建语法树
3. 如果不符合,报告语法错误
// 一个简单表达式文法(巴科斯范式 BNF) // 产生式规则 Expr → Term Expr' Expr' → + Term Expr' | ε Expr' → - Term Expr' | ε Term → Factor Term' Term' → * Factor Term' | ε Term' → / Factor Term' | ε Factor → ( Expr ) | number
思路:从开始符号出发,逐步展开,匹配输入串
代表算法:递归下降法、LL(1)
特点:构建语法树时,从根节点开始,逐步向下构建
思路:从输入串出发,逐步归约,最终归约到开始符号
代表算法:LR(1)、LALR
特点:构建语法树时,从叶子节点开始,逐步向上归约
核心思想:为每个非终结符写一个函数,函数内部根据当前 Token 选择产生式。
工作流程:
1. 读取当前 Token(lookahead)
2. 根据当前 Token 和文法规则,决定调用哪个函数
3. 递归调用其他非终结符的函数
4. 匹配终结符时,消费当前 Token,读取下一个
// 文法(无左递归版本)
Expr → Term ExprRest
ExprRest → + Term ExprRest | - Term ExprRest | ε
Term → Factor TermRest
TermRest → * Factor TermRest | / Factor TermRest | ε
Factor → number | ( Expr )
// 递归下降法解析器(Go语言实现) package main type Parser struct { tokens []Token pos int // 当前 Token 位置 } // Expr → Term ExprRest func (p *Parser) parseExpr() *Node { term := p.parseTerm() exprRest := p.parseExprRest() return &Node{Type: "Expr", Children: []*Node{term, exprRest}} } // ExprRest → + Term ExprRest | - Term ExprRest | ε func (p *Parser) parseExprRest() *Node { if p.pos < len(p.tokens) { if p.tokens[p.pos].Type == "PLUS" { p.consume("PLUS") // 消费 '+' term := p.parseTerm() exprRest := p.parseExprRest() return &Node{Type: "+", Children: []*Node{term, exprRest}} } else if p.tokens[p.pos].Type == "MINUS" { p.consume("MINUS") // 消费 '-' term := p.parseTerm() exprRest := p.parseExprRest() return &Node{Type: "-", Children: []*Node{term, exprRest}} } } return &Node{Type: "ε"} // 空产生式 } // Term → Factor TermRest func (p *Parser) parseTerm() *Node { factor := p.parseFactor() termRest := p.parseTermRest() return &Node{Type: "Term", Children: []*Node{factor, termRest}} } // Factor → number | ( Expr ) func (p *Parser) parseFactor() *Node { if p.tokens[p.pos].Type == "NUMBER" { val := p.tokens[p.pos].Value p.consume("NUMBER") return &Node{Type: "number", Value: val} } else if p.tokens[p.pos].Type == "LPAREN" { p.consume("LPAREN") // 消费 '(' expr := p.parseExpr() p.consume("RPAREN") // 消费 ')' return expr } panic("unexpected token") }
• 简单直观,容易手写
• 错误报告准确(可以精确定位)
• 容易添加自定义语法
• Go、Python 等语言的编译器使用
• 不能处理左递归文法
• 需要手动消除左递归和提取左公因子
• 递归调用可能栈溢出(深层递归)
• 不是所有文法都能用递归下降法解析
src/cmd/compile/internal/syntax/parser.go
LL(1) 的含义:
• 第一个 L:从左到右(Left-to-right)扫描输入
• 第二个 L:最左推导(Leftmost derivation)
• 1:只需要向前看 1个 Token 就能决定使用哪个产生式
核心思想:用 分析表(Parsing Table) 指导解析过程,避免回溯。
需要计算两个集合:
1. FIRST(α):符号串 α 能推导出的 第一个终结符 的集合
2. FOLLOW(A):非终结符 A 后面可能跟随的 终结符 的集合
填充分析表:
• 对于产生式 A → α,对于每个终结符 a ∈ FIRST(α),设置 Table[A, a] = A → α
• 如果 ε ∈ FIRST(α),则对于每个终结符 b ∈ FOLLOW(A),设置 Table[A, b] = A → α
// 文法 1. E → T E' 2. E' → + T E' | ε 3. T → F T' 4. T' → * F T' | ε 5. F → ( E ) | id
| 非终结符 | id | + | * | ( | ) | $ |
|---|---|---|---|---|---|---|
| E | E → T E' | E → T E' | ||||
| E' | E' → + T E' | E' → ε | E' → ε | |||
| T | T → F T' | T → F T' | ||||
| T' | T' → ε | T' → * F T' | T' → ε | T' → ε | ||
| F | F → id | F → ( E ) |
// LL(1) 解析器(表驱动) type LL1Parser struct { table map[string]map[string]string // 分析表 stack []string // 分析栈 input []Token // 输入 Token 序列 pos int // 输入位置 } func (p *LL1Parser) parse() bool { // 初始化栈 p.stack = []string{"$", "E"} for len(p.stack) > 0 { top := p.stack[len(p.stack)-1] currentToken := p.input[p.pos].Type if isTerminal(top) { if top == currentToken { // 匹配成功,弹栈,消费 Token p.stack = p.stack[:len(p.stack)-1] p.pos++ } else { return false // 语法错误 } } else { // 非终结符,查表 production := p.table[top][currentToken] if production == "" { return false // 语法错误 } // 弹栈 p.stack = p.stack[:len(p.stack)-1] // 如果产生式不是 ε,逆序压栈 if production != "ε" { symbols := reverse(splitProduction(production)) for _, sym := range symbols { p.stack = append(p.stack, sym) } } } } return p.pos == len(p.input) // 输入是否全部消费 }
LR(1) 的含义:
• L:从左到右(Left-to-right)扫描输入
• R:最右推导的逆过程(Rightmost derivation in reverse)
• 1:向前看 1个 Token 来决定是否归约
核心思想:维护一个 状态栈 和 符号栈,通过 动作表(Action Table) 和 转移表(Goto Table) 指导解析。
两个核心数据结构:
1. 状态栈(State Stack):存储 LR(1) 自动机的状态
2. 符号栈(Symbol Stack):存储已匹配的符号
两种动作:
• 移进(Shift):将当前 Token 移入符号栈,状态栈压入新状态
• 归约(Reduce):用产生式 A → α 归约,弹出 |α| 个符号,压入 A
• 接受(Accept):输入串解析成功
• 错误(Error):无法继续解析
| 状态 | id | + | * | ( | ) | $ |
|---|---|---|---|---|---|---|
| 0 | s5 | s4 | ||||
| 1 | s6 | acc | ||||
| 2 | r2 | s7 | r2 | r2 | ||
| 3 | r4 | r4 | r4 | r4 |
| 状态 | E | T | F |
|---|---|---|---|
| 0 | 1 | 2 | 3 |
| 4 | 8 | 2 | 3 |
s5 = 移进到状态 5
r2 = 用第 2 条产生式归约
acc = 接受(解析成功)
数字 = Goto 到该状态
// LR(1) 解析器(表驱动) type LR1Parser struct { action map[int]map[string]string // 动作表 goto map[int]map[string]int // 转移表 prods []string // 产生式列表 } func (p *LR1Parser) parse(input []string) bool { stateStack := []int{0} symbolStack := []string{"$"} input = append(input, "$") pos := 0 for { state := stateStack[len(stateStack)-1] token := input[pos] action := p.action[state][token] if strings.HasPrefix(action, "s") { // 移进 nextState := parseInt(action[1:]) stateStack = append(stateStack, nextState) symbolStack = append(symbolStack, token) pos++ } else if strings.HasPrefix(action, "r") { // 归约 prodIdx := parseInt(action[1:]) prod := p.prods[prodIdx] // 格式:A → α // 弹出 |α| 个符号和状态 alphaLen := len(splitProduction(prod)) stateStack = stateStack[:len(stateStack)-alphaLen] symbolStack = symbolStack[:len(symbolStack)-alphaLen] // 压入 A A := getLHS(prod) symbolStack = append(symbolStack, A) // Goto topState := stateStack[len(stateStack)-1] nextState := p.goto[topState][A] stateStack = append(stateStack, nextState) } else if action == "acc" { return true // 解析成功 } else { return false // 语法错误 } } }
| 对比维度 | 递归下降法 | LL(1) | LR(1) |
|---|---|---|---|
| 解析方向 | 自上而下(Top-Down) | 自上而下(Top-Down) | 自下而上(Bottom-Up) |
| 实现方式 | 手写递归函数 | 表驱动(分析表) | 表驱动(Action + Goto 表) |
| 是否需要分析表 | ❌ 不需要 | ✅ 需要 LL(1) 分析表 | ✅ 需要 Action + Goto 表 |
| 能解析的文法 | LL(1) 文法(需消除左递归) | LL(1) 文法 | LR(1) 文法(更强大) |
| 是否支持左递归 | ❌ 不支持(会无限递归) | ❌ 不支持 | ✅ 支持 |
| 错误报告 | ✅ 精确(可以自定义错误信息) | ⚠️ 一般(分析表查不到就是错误) | ⚠️ 一般(需要额外处理错误恢复) |
| 实现难度 | ✅ 简单(容易手写) | ⚠️ 中等(需要构造分析表) | ❌ 复杂(需要构造 LR(1) 自动机) |
| 实际使用 | Go、Python、手写解析器 | 教学、简单语言 | Yacc/Bison、GCC、LLVM |
递归下降法 = 按食谱做菜(自上而下)
• 从"做鱼香肉丝"开始,逐步分解成"切肉"、"炒菜"等步骤
• 每遇到一个步骤,就调用对应的函数
LL(1) = 查表做菜(自上而下,但有菜谱表)
• 有一个"菜谱表",看到当前的食材,查表决定下一步做什么
• 不需要自己判断,严格按照表来
LR(1) = 从成品反推做法(自下而上)
• 看到桌上的菜(输入串),逐步归约成"这是鱼香肉丝"
• 更强大,能处理更复杂的菜谱(文法)
1️⃣ 递归下降法:手写递归函数,简单直观,Go 编译器使用
2️⃣ LL(1):表驱动,自上而下,只能解析 LL(1) 文法
3️⃣ LR(1):表驱动,自下而上,能解析几乎所有编程语言
4️⃣ 选择建议:手写解析器用递归下降法,自动生成用 LR(1)(Yacc/Bison)
🎉 恭喜你完成了语法分析算法的学习!
现在你应该理解了递归下降、LL(1)、LR(1) 三种算法的原理和实现。
# 推荐资源 # 1. 书籍 《编译原理》(龙书) - Alfred V. Aho 等 # 语法分析章节讲得非常详细 《Modern Compiler Implementation》 - Andrew Appel # 有 C、Java、ML 三个版本 # 2. 在线工具 ANTLR: https://www.antlr.org/ # 自动生成解析器 Grammar Explorer: https://mdkrajnak.github.io/grammar-explorer/ # 可视化文法 # 3. 实战项目 1. 用递归下降法写一个计算器(四则运算 + 括号) 2. 用 ANTLR 生成一个 JSON 解析器 3. 阅读 Go 编译器的 parser.go(递归下降法实战)