news 2026/6/7 6:30:05

别再死记硬背First和Follow集了!用LL(1)文法实战解析PL/0表达式(附C源码调试技巧)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再死记硬背First和Follow集了!用LL(1)文法实战解析PL/0表达式(附C源码调试技巧)

从First/Follow集到可运行分析器:PL/0表达式LL(1)解析实战指南

当你第一次翻开编译原理教材,看到"First集"、"Follow集"这些术语时,是否感觉像在阅读天书?许多同学在理论学习阶段能够勉强计算这些集合,却始终不明白它们与实际语法分析器的关系。本文将用PL/0语言的表达式作为案例,带你完整走通从文法定义到可运行分析器的全流程。

1. 为什么需要First和Follow集?

在开始技术细节前,我们先理解这些概念存在的意义。想象你正在编写一个计算器程序,需要解析用户输入的数学表达式。当遇到一个"+"号时,程序应该立即执行加法吗?不一定——它需要"向前看"下一个符号来决定当前的操作优先级。这就是预测分析的核心思想。

First集告诉我们:当看到某个符号时,可以期待哪些开始符号。例如在PL/0中:

  • 表达式的First集包含+,-, 标识符, 数字和(
  • 项的First集则包含标识符, 数字和(

Follow集则标记了:在某个非终结符之后,可能出现的合法符号。比如在PL/0中:

  • 表达式的Follow集包含), 关系运算符等
  • 项的Follow集则包含加减运算符

这些集合不是学术游戏,而是编译器实现者的实用工具。通过它们,我们可以:

  • 验证文法是否适合自顶向下分析(LL(1)条件)
  • 构建预测分析表
  • 设计递归下降分析器的控制逻辑

2. PL/0表达式文法拆解

让我们从PL/0的BNF定义开始,逐步拆解这个看似简单却蕴含深意的文法:

<表达式> ::= [+|-]<项>{<加法运算符> <项>} <项> ::= <因子>{<乘法运算符> <因子>} <因子> ::= <标识符>|<无符号整数>| '(' <表达式> ')'

2.1 文法结构分析

这个文法定义了三种关键结构:

  1. 表达式:可能带有前导符号(+/-)的项序列,通过加减号连接
  2. :因子序列,通过乘除号连接
  3. 因子:最基本的运算单元,可以是变量、常量或括号表达式

这种分层设计巧妙地处理了运算符优先级:

  • 乘除运算(项级)自然比加减(表达式级)绑定更紧密
  • 括号强制提升内部表达式的优先级

2.2 计算First集实战

让我们实际计算这些非终结符的First集。记住规则:对于产生式A → α,First(A)包含First(α)中的所有终结符。

表达式First集

  • 产生式以可选+/-开始 → 包含+,-
  • 接着是项 → 包含项的First集(标识符, 数字,(
  • 最终结果:{+, -, 标识符, 数字, (}

项First集

  • 以因子开头 → 与因子First集相同
  • 结果:{标识符, 数字, (}

因子First集

  • 直接包含三种情况的开始符号
  • 结果:{标识符, 数字, (}

提示:当文法存在ε产生式时,需要额外考虑Follow集,但PL/0表达式文法不涉及这种情况。

2.3 Follow集计算要点

Follow集计算相对复杂,需要追踪非终结符在产生式中的出现位置。关键规则:

  1. 对于A → αBβ,Follow(B)包含First(β)的非ε元素
  2. 如果β可推导出ε,或A → αB,则Follow(B)包含Follow(A)

表达式Follow集

  • 出现在因子中的(表达式)→ 包含)
  • 整个程序的顶层表达式 → 包含输入结束符$
  • 结果:{), $, 关系运算符}

项Follow集

  • 出现在表达式中的项 加减符...→ 包含加减符
  • 也继承表达式的Follow集
  • 结果:{+, -, ), $, 关系运算符}

因子Follow集

  • 出现在项中的因子 乘除符...→ 包含乘除符
  • 也继承项的Follow集
  • 结果:{*, /, +, -, ), $, 关系运算符}

3. LL(1)条件验证

有了First和Follow集,我们可以验证这个文法是否满足LL(1)条件。LL(1)文法要求:

  1. 无左递归
  2. 对于每个非终结符A和它的每个产生式A→α,First(α)互不相交
  3. 如果α能推导出ε,则First(α)与Follow(A)不相交

PL/0表达式分析

  1. 明显没有左递归
  2. 每个非终结符的各产生式First集互斥:
    • 因子的三个选择:标识符、数字、(— 完全不相交
  3. 无ε产生式,第三条自动满足

因此,这个文法是LL(1)的,适合用递归下降或预测分析法实现。

4. 递归下降分析器实现

理论验证后,我们进入实战环节——用C语言实现递归下降分析器。递归下降的核心思想是将每个非终结符实现为一个函数,根据当前输入符号决定调用哪个产生式。

4.1 基础框架

#include <stdio.h> #include <ctype.h> char lookahead; // 当前查看的字符 void match(char expected) { if (lookahead == expected) { lookahead = getchar(); } else { fprintf(stderr, "语法错误: 期望 '%c' 但得到 '%c'\n", expected, lookahead); exit(1); } } void expression(); // 前向声明

4.2 因子解析

我们从最底层的因子开始实现:

void factor() { if (isalpha(lookahead)) { // 标识符 match(lookahead); } else if (isdigit(lookahead)) { // 数字 while (isdigit(lookahead)) { match(lookahead); } } else if (lookahead == '(') { // 括号表达式 match('('); expression(); match(')'); } else { fprintf(stderr, "语法错误: 意外的字符 '%c'\n", lookahead); exit(1); } }

4.3 项解析

项处理乘除运算,注意使用循环处理连续运算:

void term() { factor(); while (lookahead == '*' || lookahead == '/') { match(lookahead); factor(); } }

4.4 表达式解析

顶层表达式处理加减运算和可选前导符号:

void expression() { if (lookahead == '+' || lookahead == '-') { match(lookahead); } term(); while (lookahead == '+' || lookahead == '-') { match(lookahead); term(); } }

4.5 主程序

int main() { lookahead = getchar(); expression(); if (lookahead != '\n' && lookahead != EOF) { fprintf(stderr, "语法错误: 有多余字符 '%c'\n", lookahead); return 1; } printf("语法正确\n"); return 0; }

5. 调试技巧与边界案例

实现分析器只是开始,如何验证它的正确性?精心设计的测试用例至关重要。

5.1 测试用例设计

好的测试应覆盖:

  • 正常情况:各种合法表达式组合
  • 边界情况:极简表达式、嵌套深度大的表达式
  • 错误情况:各种可能的语法错误位置

推荐测试集

测试用例预期结果测试目的
a+15*b通过基本运算优先级
(a+15)*b通过括号改变优先级
+a--b通过前导符号和多符号
a+错误缺少右操作数
a b错误缺少运算符
((a))通过多层括号
a+*b错误连续运算符

5.2 常见错误模式

在实现递归下降分析器时,有几个典型错误需要注意:

  1. 无限递归

    • 当文法存在间接左递归时容易发生
    • 例如:A → Bα,B → Aβ
    • PL/0表达式文法已避免此问题
  2. 预测冲突

    • 当两个产生式的First集有重叠时发生
    • 正确的LL(1)文法应避免这种情况
  3. 错误恢复不足

    • 简单的exit(1)不利于用户体验
    • 实际编译器应尝试恢复并继续分析

5.3 调试递归下降分析器

当分析器行为不符合预期时,可以:

  1. 添加调试输出:
void expression() { printf("进入expression(), lookahead='%c'\n", lookahead); // ...原有代码... }
  1. 使用gdb等调试器逐步执行

  2. 绘制函数调用图,验证是否符合文法结构

6. 扩展:与词法分析器集成

完整的编译器需要将词法分析与语法分析结合。我们可以修改分析器,使其从词法分析器获取token而非直接处理字符。

6.1 词法分析接口

typedef enum { TOKEN_IDENT, TOKEN_NUMBER, TOKEN_PLUS, TOKEN_MINUS, TOKEN_MUL, TOKEN_DIV, TOKEN_LPAREN, TOKEN_RPAREN, TOKEN_EOF } TokenType; typedef struct { TokenType type; union { char *ident; int number; } value; } Token; Token getNextToken(); // 从输入获取下一个token

6.2 修改语法分析器

Token lookahead_token; void match(TokenType expected) { if (lookahead_token.type == expected) { lookahead_token = getNextToken(); } else { fprintf(stderr, "语法错误: 意外的token类型\n"); exit(1); } } void factor() { switch (lookahead_token.type) { case TOKEN_IDENT: match(TOKEN_IDENT); break; case TOKEN_NUMBER: match(TOKEN_NUMBER); break; case TOKEN_LPAREN: match(TOKEN_LPAREN); expression(); match(TOKEN_RPAREN); break; default: fprintf(stderr, "语法错误: factor期望标识符、数字或左括号\n"); exit(1); } }

这种分层设计使语法分析器更清晰,也更容易扩展支持更复杂的语法结构。

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

STM32裸机环境下AHT系列温湿度传感器I2C驱动工程包(含AHT10/AHT20/AHT30)

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;一套开箱即用的嵌入式温湿度传感接入方案&#xff0c;专注AHT10、AHT20、AHT30三款国产高精度传感器在STM32等Cortex-M系列MCU上的裸机驱动实现。包含已优化时序和错误重试机制的MYI2C底层驱动模块&#xff08;…

作者头像 李华
网站建设 2026/6/7 6:27:21

图数据库实战入门:三天搞定电商风控与社交推荐建模

1. 这不是又一本讲“图”的数学书——它是一份给真实业务场景用的图数据库上手指南你打开这篇文章&#xff0c;大概率不是因为刚读完《离散数学》想重温邻接矩阵&#xff0c;而是最近被某个业务问题卡住了&#xff1a;用户关系链路查得慢、推荐结果总像在猜、风控规则改一次要等…

作者头像 李华
网站建设 2026/6/7 6:25:19

Sqribble文档自动化原理:模板驱动的云原生排版流水线

1. 项目概述&#xff1a;这不是“一键生成”&#xff0c;而是一套被精心封装的文档流水线你有没有过这种经历&#xff1a;手头有一篇写得不错的博客文章&#xff0c;或者一份整理好的培训笔记&#xff0c;突然需要把它变成一本像模像样的PDF电子书——用来当课程资料、客户提案…

作者头像 李华