前言

文法规则的分析有两种方法, 一种是自顶向下,一种是自底向上。这里我们先来聊一聊自顶向下。自底向上可以看我另一篇自底向上分析总结
自顶向下包括两种方法:

  • 递归(下降)子程序
  • LL(1)

另外例子可查看

  • 自顶向下生成语法树和汇编代码
  • 使用文法规则实现四则运算

左递归

我们都知道我们要把文法规则写成递归下降子程序,要保证没有左递归和左公因子的问题(值得一提的是,尽管如此,但可能还是会有二义性的问题产生)。
看个例子吧:
A→Aa∣bA → Aa | b A→Aa∣b
有两种方式,一种是用EBNF改写:
A→b{a}A → b \{a\} A→b{a}
推导过程靠归纳得到。
另一种方式是改成右递归:
A→bA′A′→ε∣aA′A → bA' ~~~~~\\ A' → ε ~|~ aA' A→bA′     A′→ε ∣ aA′

在结束这一部分之前,给大家来一个进阶版的例子:
A→Ba∣Aa∣cB→Bb∣Ab∣dA → Ba ~|~ Aa ~|~ c \\ B → Bb ~|~ Ab ~|~ d A→Ba ∣ Aa ∣ cB→Bb ∣ Ab ∣ d
可以看到上面这个例子不止有直接左递归,还有间接左递归的问题。这个时候我们可以用这样的方法:

    1. 从上往下,消除直接左递归
    1. 看下一条文法规则,若存在上面文法的左边的非终结符号,就把上面得到的不含直接左递归的文法规则代入到这条规则中,再消去左递归即可。

接下来我们动手做一下,先把第一条规则转换一下, 比如这里采用右递归的方式改写:
A→BaA′∣cA′A′→aA′∣εB→Bb∣Ab∣dA → BaA' ~|~ cA' \\ A' → aA' ~|~ ε ~~~~~~~~~\\ B → Bb ~|~ Ab ~|~ d ~~ A→BaA′ ∣ cA′A′→aA′ ∣ ε         B→Bb ∣ Ab ∣ d  
接下来把 B在左边的文法规则中的A用第一条文法规则代入
A→BaA′∣cA′A′→aA′∣εB→Bb∣BaA′b∣CA′b∣dA → BaA' ~|~ cA' ~~~~~~~~~~~~~~~~~\\ A' → aA' ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~~~\\ B → Bb ~|~ BaA'b ~|~ CA'b ~|~ d A→BaA′ ∣ cA′                 A′→aA′ ∣ ε                         B→Bb ∣ BaA′b ∣ CA′b ∣ d
接下来再消去第三条文法规则中的直接左递归
A→BaA′∣cA′A′→aA′∣εB→CA′bB′∣dB′B′→bB′∣aA′bB′∣εA → BaA' | cA' ~~~~~~~~~\\ A' → aA' | ε ~~~~~~~~~~~~~~~~~\\ B → CA'bB' | dB' ~~~~~\\ B' → bB' ~|~ aA'bB' ~|~ ε A→BaA′∣cA′         A′→aA′∣ε                 B→CA′bB′∣dB′     B′→bB′ ∣ aA′bB′ ∣ ε
这样就消去了直接左递归和间接左递归了。

左公因子

这里给个简单的例子:
A→αβ∣αγA → αβ ~|~ αγ A→αβ ∣ αγ
改的方法很简单:
A→αA′A′→β∣γA → αA' ~\\ A' → β ~|~ γ A→αA′ A′→β ∣ γ

依然我们这里来个升级版的例子:
S→bB∣ACcA→aA∣bB∣εB→e∣dC→f∣εS → bB ~|~ ACc ~~\\ A → aA ~|~ bB ~|~ ε \\ B → e ~|~ d ~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~ S→bB ∣ ACc  A→aA ∣ bB ∣ εB→e ∣ d           C→f ∣ ε          
这里有间接左递归: S → ACc ,然后A → bB, 发现会与 S → bB产生二义性。其实我们消去左公因子和左递归的本质就是为了程序能够运行下去。照着含有左递归的文法直接写出来的递归下降子程序会产生栈溢出的问题,而按着左公因子的文法则会让程序具有二义性…
解决方法是:
将第二条文法规则代入第一条即可.
S→bB∣aACc∣bBCc∣CcS → bB ~|~ aACc ~|~ bBCc ~|~ Cc S→bB ∣ aACc ∣ bBCc ∣ Cc
在提取左公因子
S→bB(ε∣Cc)∣aACc∣CcS → bB(ε ~|~ Cc) ~|~ aACc ~|~ Cc S→bB(ε ∣ Cc) ∣ aACc ∣ Cc
最终结果:
S→bB(ε∣Cc)∣aACc∣CcA→aA∣bB∣εB→e∣dC→f∣εS → bB(ε ~|~ Cc) ~|~ aACc ~|~ Cc \\ A → aA ~|~ bB ~|~ ε ~~~~~~~~~~~~~~~~~~~~~\\ B → e ~|~ d ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ S→bB(ε ∣ Cc) ∣ aACc ∣ CcA→aA ∣ bB ∣ ε                     B→e ∣ d                                C→f ∣ ε                                
接下来我们用这个文法来写一下递归下降子程序。

递归下降子程序

写一个递归下降子程序实现下列文法规则
S→aACc∣bB(ε∣Cc)∣CcA→aA∣bB∣εB→e∣dC→f∣εS → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc \\ A → aA ~|~ bB ~|~ ε ~~~~~~~~~~~~~~~~~~~~~\\ B → e ~|~ d ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ S→aACc ∣ bB(ε ∣ Cc) ∣ CcA→aA ∣ bB ∣ ε                     B→e ∣ d                                C→f ∣ ε

方法: 遇到非终结符号就调用,遇到终结符号就匹配。
备注: 下列程序中token是全局变量。
先来看看match函数:

void match(char expectToken){if (token == expectToken){// 这里getToken指的是读取下一个字符,可以是用户输入,也可以是文件读入,也可以是一段字符串的下一个字符// 因此这里我把这个读取下一个字符保存到token中抽象为下列这句代码了token = getToken();} else {Error(); // Error函数再讲完这个例子后会仔细提一下}
};

S→aACc∣bB(ε∣Cc)∣CcS → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc S→aACc ∣ bB(ε ∣ Cc) ∣ Cc

void S(){if (token == 'a'){match('a');A();C();match('c');} else if (token == 'b'){match('b');B();// 这里涉及到求Cc的First集合待会儿会说 !!! First集合 !!!if (token == 'f' || token == 'c') {C(); // 实际上这里是可以优化的,为此会引入follow集合的概念  !!!Follow集合!!!match('c');} else if (token == 'f' || token == 'c') {C(); // 实际上这里是可以优化的,为此会引入follow集合的概念 match('c');} else {Error();}
}

A→aA∣bB∣εA → aA ~|~ bB ~|~ ε \\ A→aA ∣ bB ∣ ε

void A(){if (token == 'a'){match('a');A();} else if (token == 'b'){match('b');B();}// 由于有 ε ,这里不能判断出错
}

B→e∣dB → e ~|~ d B→e ∣ d

void B(){if (token == 'e'){match('e');} else if (token == 'd'){match('d');} else {Error();}
}

C→f∣εC → f ~|~ ε C→f ∣ ε

void C(){if (token == 'f'){match('f');}// 由于有 ε ,这里不能判断出错
}

好了,递归子程序我们就写完了。
不过,我们还要讨论一个问题, 就是我们在写
S→aACc∣bB(ε∣Cc)∣CcS → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc S→aACc ∣ bB(ε ∣ Cc) ∣ Cc

void S(){if (token == 'a'){match('a');A();C();match('c');} else if (token == 'b'){match('b');B();// 这里涉及到求Cc的First集合待会儿会说 !!! First集合 !!!if (token == 'f' || token == 'c') {C(); // 实际上这里是可以优化的,为此会引入follow集合的概念  !!!Follow集合!!!match('c');} else if (token == 'f' || token == 'c') {C(); // 实际上这里是可以优化的,为此会引入follow集合的概念 match('c');} else {Error();}
}

这个递归子程序的例子中,我们设计到两个概念,一个是First集合,一个是Follow集合。下面我们来讨论一下这个东西。

First 集合

给出一个上述例子的简化版文法
S→CcC→f∣εS → Cc ~~\\ C → f ~|~ ε S→Cc  C→f ∣ ε
我们会对第一条文法写出这样的程序

void S(){if (token == 'f' || 'c'){C();match('c');} else {Error();}
}

这里你可能会问,if (token == ’ f ’ || ‘c’) 判断条件怎么来的呢?
答案是求 Cc的First集合。
求First集合的算法如下:

First(x) = {};
k = 1;
while (k <= n){if (xk 为终结符号或 ε)first(xk) = xk;first(x) = first(x) ⋃ first(xk) - {ε};if (ε 不属于 first(xk))break;
}
if (k == n + 1)first(x) = first(x) ⋃ {ε};

Follow集合

S→CcC→f∣εS → Cc ~~\\ C → f ~|~ ε S→Cc  C→f ∣ ε
其实这里的 S 程序可以写成这个样子:

void S(){if (token == 'f'){C();match('c');} else if(token == 'c'){match('c');} else {Error();}
}

为什么要这样写呢?我们看到C可以是指向 ε 的,那么也就是调用C这个函数的时候, 只是判断一下是不是 ’ f '而已, 但是程序却为此付出了大的代价: 操作系统要保存现在,将数据断点啥的压入运行栈,而保存一下又出栈了。多不划算啊。如果我们可以早点知道C什么时候不用进去就好了。此时我们可以引入Follow集合。

void C(){if (token == 'f'){match('f');}
}

也就是说,如果我们知道Follow(C), 即C的下一个字符,那就可以判断用不用进去了。这就是Follow集合的含义。
我们看一下Follow集合的算法:

Follow(start-symbol) := {$}
for all nonterminals A is not equal to  start-symbol do Follow(A) := {}
while there are changes to any Follow sets do for each production A -> X1, X2, ..., Xn dofor each Xi that is a nonterminal doadd First(Xi+1, Xi+2...xi+n) - {ε} to Follow(Xi)if ε is in First(Xi+1Xi+2...Xn) thenadd Follow(A) to Follow(Xi)

Error函数

这里我们提一下Error函数。
Error函数不应该只是简单地和用户说出错吧。
应该说一下是哪里错误了。
我们以上文提到的例子来改写一下
S→aACc∣bB(ε∣Cc)∣CcA→aA∣bB∣εB→e∣dC→f∣εS → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc \\ A → aA ~|~ bB ~|~ ε ~~~~~~~~~~~~~~~~~~~~~\\ B → e ~|~ d ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ S→aACc ∣ bB(ε ∣ Cc) ∣ CcA→aA ∣ bB ∣ ε                     B→e ∣ d                                C→f ∣ ε

void Error(int errorId, char expectToken){char[4][200] = {"S → aACc | bB(ε | Cc) | Cc", "A → aA | bB | ε", "B → e | d", "C → f | ε"}printf("error at %s, expected token is %c but not found!", char[errorId - 1], expectToken);
}

改写上面那些函数:
match函数:

void match(char expectToken, int errorId){if (token == expectToken){// 这里getToken指的是读取下一个字符,可以是用户输入,也可以是文件读入,也可以是一段字符串的下一个字符// 因此这里我把这个读取下一个字符保存到token中抽象为下列这句代码了token = getToken();} else {Error(errorId, expectToken); // Error函数再讲完这个例子后会仔细提一下}
};

S→aACc∣bB(ε∣Cc)∣CcS → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc S→aACc ∣ bB(ε ∣ Cc) ∣ Cc

void S(){if (token == 'a'){match('a', 1);A();C();match('c');} else if (token == 'b'){match('b', 1);B();// 这里涉及到求Cc的First集合待会儿会说 !!! First集合 !!!if (token == 'f' || token == 'c') {C(); // 实际上这里是可以优化的,为此会引入follow集合的概念  !!!Follow集合!!!match('c', 1);} else if (token == 'f' || token == 'c') {C(); // 实际上这里是可以优化的,为此会引入follow集合的概念 match('c', 1);} else {Error(1, '-'); // 这里就不纠结这个细节了,给了'-', 大家可以尝试优化一下}
}

A→aA∣bB∣εA → aA ~|~ bB ~|~ ε \\ A→aA ∣ bB ∣ ε

void A(){if (token == 'a'){match('a', 2);A();} else if (token == 'b'){match('b', 2);B();}// 由于有 ε ,这里不能判断出错
}

B→e∣dB → e ~|~ d B→e ∣ d

void B(){if (token == 'e'){match('e', 3);} else if (token == 'd'){match('d', 3);} else {Error(3, '-'); // 这里就不纠结这个细节了,给了'-', 大家可以尝试优化一下}
}

C→f∣εC → f ~|~ ε C→f ∣ ε

void C(){if (token == 'f'){match('f', 4);}// 由于有 ε ,这里不能判断出错
}

LL(1) 分析方法

上面我们学会了如何书写一个递归下降子程序来实现一组文法规则,那么假如给一个文法规则,我们每次都要重新写文法规则识别程序,是比较繁琐的。有没有什么办法可以“一劳永逸”呢?
前辈们想出了用表达方法,以二维数据的方式来表达递归下降子程序的路径。
由于思想与递归下降子程序一样,因此写出LL(1)表格的时候也要先去掉左递归和左公因子的问题。
S→aACc∣bBCc∣CcA→aA∣bB∣εB→e∣dC→f∣εS → aACc ~|~ bBCc ~|~ Cc \\ A → aA ~|~ bB ~|~ ε ~~~~~~~~~~~~~\\ B → e ~|~ d~~~~~~~~~~~~~~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~ S→aACc ∣ bBCc ∣ CcA→aA ∣ bB ∣ ε             B→e ∣ d                        C→f ∣ ε

a b c d e f
S aACc bBCc Cc Cc
A aA bB
B d e
C f

这里还没完,还要计算Follow(A)和Follow(C)

  • Follow(A) = {c, f}
  • Follow(C) = {c}

然后将 ε 填入 Matrix[A][c], Matrix[A][f], Matrix[C][c]中。

a b c d e f
S aACc bBCc Cc Cc
A aA bB ε ε
B d e
C ε f

然后再用个分析栈存储一下中间结果即可,为了提高性能,存储的顺序采取倒序形式。然后访问表格过程中,如果访问到空白的单元格,如Matrix[S][d] ,则产生出错。

思考

S→aACc∣bB(ε∣Cc)∣CcA→aA∣bB∣εB→e∣dC→f∣εS → aACc ~|~ bB(ε ~|~ Cc) ~|~ Cc \\ A → aA ~|~ bB ~|~ ε ~~~~~~~~~~~~~~~~~~~~~\\ B → e ~|~ d ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\\ C → f ~|~ ε ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ S→aACc ∣ bB(ε ∣ Cc) ∣ CcA→aA ∣ bB ∣ ε                     B→e ∣ d                                C→f ∣ ε                                
用LL(1)中,Matrix[S][b]应该填什么?
(提示: 最长串匹配原则。

实战小例子

  • 使用文法规则实现四则运算

结语

这里我们回顾了一下自顶向下的知识点。自顶向下还是挺有趣的,特别是它还能解决运算符的优先级和左结合性,之前大概率都是用中缀转后缀的做法实现的。
好啦,有什么错误和建议欢迎大家留言哦。嘻嘻。

参考资料: 编译原理及实践 Kenneth.C.Louden 机械工业出版社 2000.3

文法规则自顶向下分析相关推荐

  1. 编译原理 自顶向下分析

    从顶部的根节点到底部的叶节点分析方法叫做自顶向下分析.我们知道顶部的根节点可以表示成一个文法的开始符号S,所以说,自顶向下分析可以看成是从文法的开始符号S推导出词串w的过程. 例如,我们以输入id + ...

  2. 语法分析 自顶向下分析

    语法分析 自顶向下分析 一.确定的自顶向下分析思想 : 确定的自顶向下分析方法,首先要解决从某文法的开始符号出发,对给定的输入符号串如何根据当前的输入符号(单词符号)唯一地确定选用哪个产生式替换相应非 ...

  3. 自顶向下分析消除左递归的方法

    左递归产生的原因是产生式的左右有相同的非终结符,具体来说就是形如 A→Aα | β 这时自顶向下分析将成为死循环,消除左递归的方法是引入符号A'和ε A→βA' A'→αA' | ε 以上是直接左递归 ...

  4. 文法G[E]分析表分析字符串(i+)-编译原理

    已知文法G[E]分析表(如下所示) ) 下面来分析(i+) 首先在分析区填入#E,余留下输入串为(1+)#,所用产生式查上表:E行(列,所以为:E->TE' 如下图所示: 随后E出栈,所用产生式 ...

  5. BIM建筑环境规则和分析(BERA)语言介绍(三)第二章 背景

    本章是一份调查,旨在回顾计算机语言设计和 实现方面的相关工作,并介绍了设计和开发BERA 语言的经验教训.根据编程语言历史网站[HOPL,2010], 其数据库列出了8,512种计算机语言,其中17, ...

  6. BIM建筑环境规则和分析(BERA)语言介绍(四)第三章 BERA语言设计

    设计策略 什么是好的语言设计?没有明确的答案,但许多研究人员 声称,新语言有重要因素可以获得接受和 长寿.就领域特定语言的主要目的而言,新语言 应首先以一种简单的方式有效地解决新问题[Mashey,2 ...

  7. 【20200429】编译原理课程课业打卡十九之判断OPG文法求解句子分析过程

    [20200429]编译原理课程课业打卡十九之判断OPG文法&求解句子分析过程 一.课业打卡十九之判断OPG文法&求解句子分析过程 二.知识巩固 1.判断OPG文法 2.求算符优先函数 ...

  8. BIM建筑环境规则和分析(BERA)语言介绍(一)概要

    -------作者:JIN KOOK LEE   指导: Charles M. Eastman 本研究旨在设计和实现特定领域的计算机 编程语言:建筑环境规则和分析(BERA)语言. 由于建筑信息模型( ...

  9. 八、非规则组织分析及其数学模型——平纹变化组织

    非规则组织顾名思义,无法通过一个数学模型来描述所有的非规则组织.对于每一个具体的非规则组织而言,其也有一定的规律性可循,即可通过分析每一个具体的非规则组织的组织点运动规律来建立相应的数学模型. 一.平 ...

  10. 软件智能:aaas系统 度量衡及文法规则

    在前面的录音文件"可观察的现象和突现的过程本体-由存在的现象探索存在的真到转向存在的历史"中曾给出对现象世界的一个归纳和总结:"参差不齐的现象世界暴露出三个钉和八个爪,向 ...

最新文章

  1. 一览六月最热的5篇AI技术论文
  2. 光电耦合NEC2051 的输入输出特性
  3. 决策树模型组合之随机森林与GBDT
  4. Struts2文件上传的大小限制问题
  5. python机器人算法_DBscan算法及其Python实现
  6. 《集体智慧编程》笔记(2 / 12):提供推荐
  7. 服务端渲染与 Universal React App
  8. grep从文件末尾开始找_新人自学前端到什么程度才能找工作?
  9. 局域网远控软件DameWareNT6500
  10. Open vSwitch(OVS)介绍、编译安装与原理
  11. pandownload事件_pandownload被执法背后是中国盗版的末路
  12. @primary注解_springboot整合redis分别实现手动缓存和注解缓存
  13. PHP5 Session 使用详解(一)
  14. python生产和消费模型_【Python】python 生产/消费模型
  15. MFC选择目录和多个文件
  16. 【计算机视觉】BOF图像检索
  17. 依次计算一系列给定字符串的字母值,字母值为字符串中每个字母对应的编号值(A对应1,B对应2,以此类推,不区分大小写字母,非字母字符对应的值为0)的总和
  18. Rapid SCADA中文使用说明书(一)
  19. ESP32-cam使用-智能家居云端视频监控实现
  20. 如何在 JavaScript 中使用对象解构

热门文章

  1. java 怎么使用 设计模式对业务进行解耦(一)
  2. 海词词典android v3.1.2新版发布 英语学习必备工具,海词词典Android V3.1.2新版发布 英语学习必备工具...
  3. linux下如何创建oracle数据库实例,Linux下新建Oracle数据库实例
  4. RedHat免费订阅账号注册方式
  5. RiceQuant和 JoinQuant合成月k线、周k线的极简公式
  6. 马尔可夫链 ▏小白都能看懂的马尔可夫链详解
  7. 一篇文搞懂《AOP面向切面编程,DevOps生命周期
  8. STM32F4xx固件库分析
  9. SpringBoot_MD5加密工具类
  10. svn 回滚文件修改