news 2026/7/11 2:23:25

递归下降子程序 Python 实现:手写 Parser 解析习题4.2表达式文法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
递归下降子程序 Python 实现:手写 Parser 解析习题4.2表达式文法

递归下降子程序 Python 实现:手写 Parser 解析表达式文法

1. 从理论到实践的跨越

当我们谈论编译原理时,很多人脑海中浮现的是晦涩难懂的数学符号和抽象概念。但今天,我们将打破这种刻板印象,用Python代码将编译原理中最经典的递归下降分析法具象化。不同于教科书上的纯理论推导,本文会带你用不到200行代码实现一个能实际解析加减运算的语法分析器。

递归下降分析法的核心思想很简单:为文法中的每个非终结符编写一个对应的解析函数。这些函数会相互调用,形成一种"下降"的解析过程。想象你在解一道数学题时,先把大问题拆解成小问题,再逐个击破——这正是递归下降的精髓。

在开始编码前,我们需要明确几个关键概念:

  • 终结符:不能再被拆分的语法单元,如数字、运算符
  • 非终结符:可以由其他符号组成的语法结构,如表达式
  • FIRST集:一个非终结符可能推导出的首个终结符集合
  • FOLLOW集:可能跟随在某个非终结符后的终结符集合

2. 表达式文法解析

我们以王原生《编译原理》习题4.2的表达式文法为例:

E → TE' E' → +E | ε T → FT' T' → T | ε F → PF' F' → *F' | ε P → (E) | a | b | ∧

这个文法虽然看起来复杂,但拆解后其实很直观:

  • E代表表达式,由项T和后续表达式E'组成
  • E'处理加法运算
  • T代表项,由因子F和后续项T'组成
  • F代表因子,由基本单元P和后续因子F'组成
  • P是最基本的单元,可以是括号表达式或变量

关键点:这个文法是LL(1)的,意味着我们只需要向前看一个token就能确定使用哪个产生式,无需回溯。

3. Python实现核心架构

我们先搭建解析器的骨架代码:

class Parser: def __init__(self, tokens): self.tokens = tokens self.current = 0 self.errors = [] def parse(self): try: return self.E() except ParseError as e: self.errors.append(str(e)) return None def E(self): """表达式 E → TE'""" left = self.T() return self.E_prime(left) def E_prime(self, left): """E' → +E | ε""" if self.match('+'): operator = self.previous() right = self.E() return Binary(left, operator, right) return left # 其他方法类似...

这个架构有几个关键设计:

  1. Token流处理:将输入字符串转换为token序列
  2. 错误恢复:遇到错误时记录而非直接崩溃
  3. 递归调用:各非终结符对应的函数相互调用

4. 完整递归下降实现

下面是完整的解析器实现,包含所有非终结符的处理:

import sys from enum import Enum, auto class TokenType(Enum): NUMBER = auto() PLUS = auto() STAR = auto() LEFT_PAREN = auto() RIGHT_PAREN = auto() IDENTIFIER = auto() EOF = auto() class Token: def __init__(self, type, lexeme, literal, line): self.type = type self.lexeme = lexeme self.literal = literal self.line = line class Expr: pass class Binary(Expr): def __init__(self, left, operator, right): self.left = left self.operator = operator self.right = right class Grouping(Expr): def __init__(self, expression): self.expression = expression class Literal(Expr): def __init__(self, value): self.value = value class Variable(Expr): def __init__(self, name): self.name = name class Parser: def __init__(self, tokens): self.tokens = tokens self.current = 0 def parse(self): try: return self.expression() except ParseError: return None def expression(self): return self.E() def E(self): left = self.T() return self.E_prime(left) def E_prime(self, left): if self.match(TokenType.PLUS): operator = self.previous() right = self.E() return Binary(left, operator, right) return left def T(self): left = self.F() return self.T_prime(left) def T_prime(self, left): if self.check(TokenType.STAR): operator = self.advance() right = self.T() return Binary(left, operator, right) return left def F(self): left = self.P() return self.F_prime(left) def F_prime(self, left): if self.match(TokenType.STAR): operator = self.previous() right = self.F_prime(left) # 右递归处理 return Binary(left, operator, right) return left def P(self): if self.match(TokenType.LEFT_PAREN): expr = self.expression() self.consume(TokenType.RIGHT_PAREN, "Expect ')' after expression.") return Grouping(expr) if self.match(TokenType.IDENTIFIER): return Variable(self.previous().literal) if self.match(TokenType.NUMBER): return Literal(self.previous().literal) raise self.error(self.peek(), "Expect expression.") # 辅助方法... def match(self, *types): for type in types: if self.check(type): self.advance() return True return False def check(self, type): if self.is_at_end(): return False return self.peek().type == type def advance(self): if not self.is_at_end(): self.current += 1 return self.previous() def is_at_end(self): return self.peek().type == TokenType.EOF def peek(self): return self.tokens[self.current] def previous(self): return self.tokens[self.current - 1] def consume(self, type, message): if self.check(type): return self.advance() raise self.error(self.peek(), message) def error(self, token, message): print(f"[line {token.line}] Error at '{token.lexeme}': {message}") return ParseError()

5. 词法分析与FIRST/FOLLOW集

递归下降分析器的正确性依赖于FIRST和FOLLOW集。让我们计算关键非终结符的集合:

FIRST集计算

FIRST(E) = FIRST(T) = FIRST(F) = FIRST(P) = { '(', identifier, number } FIRST(E') = { '+', ε } FIRST(T') = { '*', ε } FIRST(F') = { '*', ε }

FOLLOW集计算

FOLLOW(E) = { ')', EOF } FOLLOW(E') = FOLLOW(E) = { ')', EOF } FOLLOW(T) = FIRST(E') ∪ FOLLOW(E') = { '+', ')', EOF } FOLLOW(T') = FOLLOW(T) = { '+', ')', EOF } FOLLOW(F) = FIRST(T') ∪ FOLLOW(T') = { '*', '+', ')', EOF } FOLLOW(F') = FOLLOW(F) = { '*', '+', ')', EOF } FOLLOW(P) = FIRST(F') ∪ FOLLOW(F') = { '*', '+', ')', EOF }

这些集合决定了何时选择ε产生式。例如在E'中,当看到')'或EOF时选择ε,因为它们在FOLLOW(E')中。

6. 测试与验证

让我们测试解析器处理不同表达式的情况:

# 测试用例 test_cases = [ ("a + b", True), ("a * b + c", True), ("a * (b + c)", True), ("a +", False), # 不完整表达式 ("(a + b", False) # 括号不匹配 ] for src, should_pass in test_cases: scanner = Scanner(src) tokens = scanner.scan_tokens() parser = Parser(tokens) expr = parser.parse() if (expr is not None) == should_pass: print(f"✓ Test passed: {src}") else: print(f"✗ Test failed: {src}") if expr is None: print(" Got None, expected parse tree") else: print(" Got parse tree, expected None")

输出示例

✓ Test passed: a + b ✓ Test passed: a * b + c ✓ Test passed: a * (b + c) ✗ Test failed: a + Got None, expected parse tree ✗ Test failed: (a + b Got None, expected parse tree

7. 错误处理与恢复

良好的错误处理能让解析器更健壮。我们在Parser类中添加错误恢复机制:

class Parser: def __init__(self, tokens): self.tokens = tokens self.current = 0 self.had_error = False def synchronize(self): """错误恢复:跳过当前语句""" self.advance() while not self.is_at_end(): if self.previous().type == TokenType.SEMICOLON: return if self.peek().type in ( TokenType.CLASS, TokenType.FUN, TokenType.VAR, TokenType.FOR, TokenType.IF, TokenType.WHILE, TokenType.PRINT, TokenType.RETURN ): return self.advance() def error(self, token, message): if token.type == TokenType.EOF: report(token.line, " at end", message) else: report(token.line, f" at '{token.lexeme}'", message) return ParseError()

这种同步恢复策略能帮助解析器在遇到错误后继续分析后续代码,而非直接停止。

8. 性能优化与实践技巧

虽然递归下降解析器直观易懂,但在处理复杂文法时可能遇到性能问题。以下是几个优化方向:

1. 记忆化(Memoization)

from functools import lru_cache class Parser: @lru_cache(maxsize=None) def E(self): left = self.T() return self.E_prime(left)

2. 尾递归优化: 对于右递归的产生式(如F' → *F' | ε),可改写为循环:

def F_prime(self, left): while self.match(TokenType.STAR): operator = self.previous() right = self.F() left = Binary(left, operator, right) return left

3. 预读缓存

def __init__(self, tokens): self.tokens = tokens self.current = 0 self.lookahead = [None] * 3 # 3个token的预读缓存 self.fill_lookahead()

递归下降分析法虽然简单,但能处理大多数LL(1)文法。当需要处理更复杂的文法时,可以考虑使用更强大的解析器生成工具如ANTLR。但对于学习编译原理和实现小型DSL来说,手写递归下降解析器仍是最佳入门选择。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/11 2:21:47

AgentScope Java 项目入门 Builder 深度解读

第一部分:AgentScope Java 项目入门 一、项目定位 AgentScope Java 是阿里巴巴开源的 AI 智能体编程框架(Apache-2.0,JDK 17),用于构建基于大语言模型(LLM)的生产级应用。可以类比为 Java 版的 …

作者头像 李华
网站建设 2026/7/11 2:21:29

让README “活”起来:Readme Typing SVG 介绍与本地化改造实践

当静态的 README 文件拥有了动态的打字效果,你的 README 主页便有了生命力。 在一些开源项目上,README 文件是项目的门面,也是开发者展示个人品牌的重要窗口。一个好的 README 不仅能清晰传达项目信息,更能给访客留下深刻印象。re…

作者头像 李华
网站建设 2026/7/11 2:18:33

AI 小说创作新手实战指南

写小说最让人头疼的往往不是灵感枯竭,而是如何将脑海中那个宏大的世界和鲜活的人物,真正落地成几十万字的连贯文本。很多创作者卡在“想得好”却“写不出”的困境里:要么开篇惊艳后续乏力,要么人物说着同样的话,剧情在…

作者头像 李华
网站建设 2026/7/11 2:16:09

彻底解决Unity WebGL加载错误:从HTTP压缩原理到服务器配置实战

1. 项目概述:一个让无数开发者头疼的WebGL加载错误如果你正在开发Unity WebGL项目,并且满怀期待地将构建好的文件上传到服务器,结果在浏览器里只看到一个白屏,控制台里赫然躺着一条“Unable to parse Build/WebGL.framework.js.gz…

作者头像 李华
网站建设 2026/7/11 2:15:43

Flutter/React Native 跨平台开发:3步配置 Android SDK 环境变量避坑指南

Flutter/React Native 跨平台开发:3步配置 Android SDK 环境变量避坑指南跨平台移动开发框架如Flutter和React Native极大提升了开发效率,但环境配置问题常常成为新手的第一道门槛。特别是当flutter doctor命令提示"ANDROID_HOME not set"时&a…

作者头像 李华
网站建设 2026/7/11 2:15:20

Agent Memory 设计笔记:从短期上下文到长期经验

echo-agent 前身为 2025 年 11 月启动的个人助理项目 fubot,最初面向长期陪伴型个人智能体,围绕认知记忆、上下文延续、用户偏好沉淀、任务闭环与持续自我优化展开。随着真实场景迭代,项目逐步形成多入口接入、统一事件模型、消息总线、Agent…

作者头像 李华