news 2026/5/19 3:13:01

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再死记硬背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)分析工作原理的最佳方式。

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

‌技术赎罪券:花钱消除代码罪孽的测试标准‌

一、“技术赎罪券”:软件测试界的现实困境在中世纪的欧洲,“赎罪券”曾是教会宣称能以金钱免除罪孽的工具,而在如今的软件测试领域,一种类似的荒诞场景正在上演:当代码中出现漏洞、逻辑缺陷或性能问题时,部…

作者头像 李华
网站建设 2026/5/19 3:12:44

高通UEFI Display驱动移植:从协议解析到屏幕点亮

1. 高通UEFI Display驱动基础解析 第一次接触高通平台的UEFI Display驱动时,我被各种Protocol和代码结构绕得头晕。后来发现,理解这套机制就像拼乐高——只要找到关键连接件,整个架构就会变得清晰。UEFI的Display驱动主要运行在XBL&#xff0…

作者头像 李华