当前位置: 首页 > news >正文

别再死记硬背FIRST和FOLLOW集了!用Python手写一个LL(1)语法分析器帮你彻底搞懂

用Python实战LL(1)语法分析器:从FIRST集到预测分析表的全流程解密

当你第一次翻开编译原理教材,看到那些密密麻麻的FIRST集、FOLLOW集公式时,是否感到一阵眩晕?别担心,你不是一个人。大多数人在学习语法分析时都会陷入"理解公式→死记硬背→考试遗忘"的怪圈。今天,我们将用Python构建一个真实的LL(1)语法分析器,让这些抽象概念在你眼前活起来。

1. 环境准备与文法定义

在开始编码前,我们需要明确几个核心概念。LL(1)分析器之所以能高效工作,关键在于它能通过一个符号的前瞻(那个"1"的含义)确定使用哪条产生式。要实现这一点,我们需要三类关键数据:FIRST集(一个符号能推导出的首终结符)、FOLLOW集(一个非终结符后可能出现的符号)以及由此计算出的SELECT集。

让我们从一个简单的四则运算文法开始,这是理解LL(1)分析的绝佳示例:

grammar = { 'E': [['T', "E'"]], "E'": [['+', 'T', "E'"], ['ε']], 'T': [['F', "T'"]], "T'": [['*', 'F', "T'"], ['ε']], 'F': [['(', 'E', ')'], ['id']] }

这个文法已经过消除左递归处理——这是LL(1)分析的前提条件。注意我们使用了'符号表示派生非终结符(如E'),这是编译原理中的常见约定。

提示:ε代表空串,在Python中我们可以用None或空列表表示,但为了清晰起见,这里保留ε符号。

2. 计算FIRST集的Python实现

FIRST集的计算看似简单,实则暗藏玄机。其核心规则是:如果一个符号能推导出空串,那么我们需要继续查看后续符号。以下是递归实现的Python代码:

def compute_first(grammar): first = {key: set() for key in grammar} changed = True while changed: changed = False for non_term in grammar: for production in grammar[non_term]: for symbol in production: if symbol in grammar: # 非终结符 before = len(first[non_term]) first[non_term].update(first[symbol] - {'ε'}) if 'ε' not in first[symbol]: break after = len(first[non_term]) if after > before: changed = True else: # 终结符 before = len(first[non_term]) first[non_term].add(symbol) after = len(first[non_term]) if after > before: changed = True break else: # 所有符号都能推导出ε first[non_term].add('ε') return first

这个实现有几个关键点:

  • 使用集合运算来避免重复元素
  • 通过changed标志检测是否需要进行下一轮迭代
  • 处理ε产生式时的特殊逻辑

运行这个函数,你会得到类似这样的输出:

{ 'E': {'(', 'id'}, "E'": {'+', 'ε'}, 'T': {'(', 'id'}, "T'": {'*', 'ε'}, 'F': {'(', 'id'} }

3. FOLLOW集计算的工程化实现

FOLLOW集的计算更加复杂,因为它需要考虑文法中符号的上下文关系。以下是分步实现的Python代码:

def compute_follow(grammar, first, start_symbol='E'): follow = {key: set() for key in grammar} follow[start_symbol].add('$') # 结束符号 changed = True while changed: changed = False for non_term in grammar: for production in grammar[non_term]: trail = follow[non_term] for symbol in reversed(production): if symbol in grammar: before = len(follow[symbol]) follow[symbol].update(trail) if 'ε' in first[symbol]: trail.update(first[symbol] - {'ε'}) else: trail = first[symbol] if before != len(follow[symbol]): changed = True else: trail = {symbol} return follow

这个算法的精妙之处在于:

  1. 从右向左遍历产生式,逐步构建FOLLOW关系
  2. 处理ε产生式时的特殊逻辑
  3. 迭代直到没有新的元素加入任何FOLLOW集

对于我们的四则运算文法,输出结果应该是:

{ 'E': {')', '$'}, "E'": {')', '$'}, 'T': {'+', ')', '$'}, "T'": {'+', ')', '$'}, 'F': {'*', '+', ')', '$'} }

4. 构建预测分析表的关键步骤

有了FIRST和FOLLOW集,我们就可以构建LL(1)分析的核心——预测分析表。这个表告诉我们在任何给定的非终结符和输入符号组合下,应该选择哪条产生式。

def build_parsing_table(grammar, first, follow): table = {} for non_term in grammar: table[non_term] = {} for production in grammar[non_term]: first_of_prod = set() for symbol in production: if symbol in grammar: first_of_prod.update(first[symbol] - {'ε'}) if 'ε' not in first[symbol]: break else: first_of_prod.add(symbol) break else: first_of_prod.add('ε') for term in first_of_prod - {'ε'}: table[non_term][term] = production if 'ε' in first_of_prod: for term in follow[non_term]: table[non_term][term] = production return table

生成的预测分析表可以用以下结构表示:

非终结符id+*()$
ET E'T E'
E'+ T E'εε
TF T'F T'
T'ε* F T'εε
Fid( E )

5. 实现表驱动分析器

现在,我们终于可以编写LL(1)分析器的主体部分了。这个分析器将使用我们构建的预测分析表来处理输入字符串。

def ll1_parse(input_string, grammar, parsing_table, start_symbol='E'): stack = ['$', start_symbol] input_string = input_string + '$' pointer = 0 while stack: top = stack[-1] current_input = input_string[pointer] if top == current_input == '$': print("Accept!") return True elif top == current_input: stack.pop() pointer += 1 elif top in grammar: if current_input in parsing_table[top]: production = parsing_table[top][current_input] stack.pop() if production != ['ε']: stack.extend(reversed(production)) else: print(f"Error: no production for {top} on {current_input}") return False else: print(f"Error: unexpected symbol {current_input}") return False return False

让我们测试一下这个分析器:

# 测试用例 test_cases = [ "id+id*id", # 有效 "(id+id)*id", # 有效 "id+*id", # 无效 "id++id", # 无效 ] for test in test_cases: print(f"\nParsing: {test}") ll1_parse(test.replace(' ', ''), grammar, parsing_table)

你会看到分析器能正确识别合法表达式并拒绝非法输入。更重要的是,通过单步调试,你可以直观地看到分析栈和输入流的变化,这正是理解LL(1)分析工作原理的最佳方式。

http://www.jsqmd.com/news/842322/

相关文章:

  • 助力美i拓客模式开发介绍【代码)
  • RTX51银行切换模式1运行时错误分析与解决方案
  • HarmonyOS ArkWeb 系列之组件四种加载方式:loadUrl、loadData、rawfile 和 resource 协议完全指南
  • 别再只会用Audition变调器了!iZotope算法和Audition算法到底怎么选?保姆级对比指南
  • 如何高效推动区域科技创新成果转化?
  • SARScape 5.6 踩坑实录:DEM导入报错?可能是这个文件后缀在捣鬼
  • NotebookLM数学研究辅助实战手册(从LaTeX建模到自动定理生成)
  • ZYNQ --- Linux成长之路 --- 从VDMA到FrameBuffer:LCD驱动的实战解析
  • Audiveris:如何将纸质乐谱快速转换为可编辑数字格式的完整指南
  • 2026年降AIGC全指南:10款降AI工具深度实测,手把手教你保留格式降低AI率 - 降AI实验室
  • 不止于对比实验:用PlatEMO 3.0的GUI模式高效调试你的自定义算法
  • UE5.1 C++项目编译太慢?试试修改这个XML文件,我的编译时间从6秒降到了1.5秒
  • 嵌入式Linux SPI调试:手把手教你用spidev_test和spi-tools搞定硬件通信
  • 从10M到1G:深入拆解Xilinx TEMAC IP核的接口选择与配置陷阱(MII/GMII/RGMII/SGMII全解析)
  • 2026年钦州权威黄金回收机构TOP5实测排行:崇左黄金回收/防城港黄金回收/南宁黄金回收/桂林黄金回收/百色黄金回收/选择指南 - 优质品牌商家
  • ncmdump解密指南:3分钟掌握网易云NCM格式转换核心技术
  • 科研党必备:用wget批量下载Zenodo数据集,告别手动点击的烦恼
  • 企业微信欢迎语功能教程:新客户添加后如何自动触达?
  • 5GC核心网元入门:从AMF到UPF,一张图看懂5G网络里的‘新部门’都是干啥的
  • Windows 11 LTSC 如何快速添加微软商店?3分钟一键部署教程
  • Trinket驱动I2C LCD与DHT22:极简引脚实现温湿度监测
  • Windows Server 2016上Winmail邮件服务器搭建保姆级教程(含虚拟机环境配置与内外网测试)
  • 3分钟让你的安卓手机变身万能键盘鼠标:USB HID Client实用指南
  • Qt 知识点及简易思维导图
  • 399裂变模式开发介绍【系统代码】
  • SAP 实战篇:Script脚本进阶,从录制到智能循环批量处理
  • 告别create_ap:在Ubuntu 22.04上用NetworkManager原生配置WiFi热点(不断开原有连接)
  • 2026年Q2郴州黄金回收鉴定机构排行实测:郴州银元回收鉴定/郴州各类名酒回收/郴州名表回收/郴州名酒回收鉴定/选择指南 - 优质品牌商家
  • 2026年5月新发布:智创云客如何以GEO优化重塑四川企业营销格局? - 2026年企业推荐榜
  • 终极解密:快速将QQ音乐加密格式转换为MP3/FLAC的完整指南