从上下文无关文法(CFG)到语法分析树——LL(1)分析法

来源:互联网 发布:女生必知护肤技巧少女 编辑:程序博客网 时间:2024/05/16 18:12

回顾


在开始之前,我们需要知道自己在做什么。
之前,我们花了不少时间熟悉如何将RE转化为DFA,那么为什么要将RE转换为DFA?这个问题类似于现在为什么要将CFG(上下文无关文法)转换成Parser。在掌握具体的步骤之前,我们有必要说明一下这样做的目的。
首先看一下编译器的模型图:
compiler
为什么要将RE转换到DFA?
相信看过实验一之后这个问题已经非常清楚。实验一我们需要完成一个词法分析器。我们需要自己用正则表达式定义一个语言的成分,比如单词、数字、注释、引用等,分别写出他们对应的NFA,合并NFA并转换成DFA,在得到DFA之后我们可以知道所有的状态和每个状态遇到不同的输入应该如何跳转处理,从DFA生成词法分析的程序。在上一篇文章的一开始,我们也提到了如何从DFA生成程序。词法分析器最终的输出是一个Token序列,记录了lexeme(词)、catalog(种类,就是自己用RE定义的)以及InnnerCode(内部码)
为什么要将CFG转到Parser?
之前我们所做的是词法分析,现在我们需要考虑语法分析的问题,当我们已经定义出一个语言的上下文无关文法,并且拿到了已经分析好的单词序列,我们怎样解析这些单词序列之间的语法关系呢?这类似于如何从有序的单词序列中分析出主语、谓语、宾语。我们需要知道这个单词对应语法中的那种成分,或者这一系列单词究竟符不符合语法要求。

递归下降分析法


有一种很简单的方法可以用来解决这个问题,它叫做递归下降分析法( Recursive Descent Parsing)。
这个方法利用递归与回搠的方法解决问题,核心在于为每个非终结符构造一个函数。

Example: S=>ABC, A=>aA|ε, B=>bB|ε, C=>cC|ε

首先是main函数:

main() {    i = S();}

为非终止状态构造函数:

int S() {    i = A();    if (i == 1) {        j = B();        if (j==1) {            return C();        } else {            error();        }    } else {        error();    }}int A() {    ch = getChar();    if (ch == 'a') return A();    else return 1;}

使用这种回搠的方法在面对复杂情况时效率很低,我们需要找出回搠的原因,最期待的情况是经过一定的预处理之后一遍就能判断对。这就需要使用另一种方法:预测分析法。

预测分析法


造成以上方法回搠,主要有两种原因:
- 公共子因子,如A=>aB|aC完全可以变成A=>aE,E=>B|C
- 左递归,如A=>Aa

需要对CFG预处理来消除这两种情况。

1、预处理

1)提取最大左因子(Extract Maximum Common Left Factor)

很简单,就和名字描述的那样,例如A=>aB|aC|bD转换成A=>aE|bD, E=>B|C。

2)消除左递归(Eliminating Left Recursion)

直接左递归

这种情况,只有一种套路。首先来看A=>Aα | β 这种情况,他代表的是以β开头,后面n个α的字符串,因此我们能得到结论:

A=>Aα|β =======> βαn ======> A=>βA’ , A’ => αA’ | ε

有这个结论,我们就可以转换所有左递归的情形。例如:E => E + T | T
把 “+T”看做α,把T看做β,上式就可以改写为 E => TE’ , E’ => +TE’| ε

间接左递归

类似 S=>Qc|c,Q=>Rb|b,R=>Sa|a这种构成了一个环,我们只要把换消除掉就好了,这里可以用到优先级的概念,比如规定S不可以推出Q和R,我们只能将S=Qc|c推倒成S=>Rbc|bc=>Sabc|abc这样就变成了直接递归,用上面的套路就可以解决了。

混合左递归

首先消除直接左递归,然后再消除间接左递归。
就是在出现圈中套圈的情况下要先把内部的圈消掉然后才能消去外部的圈。否则会永远都消不掉,陷入死循环。

2、制表

首先,我们需要再次明确我们的目的:在扫过字符串一遍后就能知道该字符的语法成分。第一步进行的预处理为这样的情况提供了可能。为了达成这一步骤,我们需要知道每个非终止符在遇到特定的字符时应该认定该字符为何种成分。为此,我们需要制定一张表格,描述了非终止符与终止符之间的关系,即如何理解输入的字符序列。

First和Follow

First和Follow都是为了达成上述目标自然形成的概念。
例如,若A=>aB, A=>bC, 那么在状态为A时扫描到a这个字符,我们则知道此时应该将A推导为aB,这个a对应的就是aB中的a。同样,若A=>BC, B=>bC,在遇到b这个字符时,我们仍然知道将A推导为BC,因为A 首先 推导出的终止符是b,这就是First的概念,描述了首先能推导出的终止符的集合。
我们这样定义First
First
若α=>α1α1…αn,则:

First(α) = First(α1) , if ε not in First(α1)
First(α) = (First(α1) - {ε0})∪First(α2) ,if ε∈First(α1)

以此类推,这里提到了ε的情况,若A=>BC, B=>ε|bB, C=>cC,那么A推出的第一个终止符就变成了b,这就是上面定义说明的意思。
可是,若是S=>AD, A=>BC, B=>ε|bB, C=>ε|cC, D=>eD这种情况呢?当我们处于A时,我们对A求First,有b和c和ε,对于这里出现的ε又如何处理呢?当我们处于A,读取的字符是e,我们仍然需要让A推导为BC,让BC都为ε,然后让A后面的D推出eD来对应这个e。所以对于A,我们还需要考虑除了First以外其他有可能在该状态出现的字符。这就要引入Follow的概念。
Follow
即若B=>αAβ,则

Follow(A) = First(β) (β不会推出ε)
Follow(A) = (First(β)-{ε})∪Follow(B) (β推出ε)

若B=>αA,则

Follow(A) <= Follow(B)

但是要是右边什么都没有了呢?如上,我们在最右边加上终止符$,它也可以是Follow的一部分。
最后,我们对经过预处理的CFG的右边求First(如果有ε则求Follow)。
示例:
Example
可以用上面的方法生成表格:
table
比如说,对于E->TE’,我们对TE’求First得到{i,(},因此i列和(列填上E->TE’。

处理

在获得这张表格之后,剩余的工作就非常简单了。
stack
首先将$和开始符E压栈(字符序列后需要加上$),若输入的第一个字符为i,查看上面的表格,E和i对应的是E=>TE’,我们就把E出栈,把E’压栈,把T压栈(注意因为是左推导,左边的需要优先处理,所以左边的要最后压栈),并记下第一步:E=>TE’,重复上面的步骤,若匹配到终止字符,则将输入的字符序列的指针后移,直到匹配到最后的终止符号为止。
之前我们记下每一步的推导式:

  1. E=>TE’
  2. T=>FT’
  3. F=>i
  4. T’=>ε
  5. E’=>+TE’
  6. T=>FT’
  7. F=>i
  8. T’=>ε
  9. E’=>ε

根据每一步的推导式可以生成上面的例子中“i+i”的语法分析树:
Tree

LL(1)语法

定义:

A grammar whose parsing table has no multiply-defined entries is said to be LL(1).

LL(1)方法是基于推导的自顶向下的方法。
- The first “L” stands for scanning the input from left to right.从左向右扫描字符串
- The second “L” stands for producing a leftmost derivation.最左推导
- “1” means using one input symbol of look-ahead s.t each step to make parsing action decisions.一步只扫描一个字符

本文的两种方法都只适用于LL(1)语法(在预处理之后符合LL(1))。

1 0
原创粉丝点击