编译原理:语法分析之上下文无关文法、LL分析

引言

语法分析器的作用:token序列->分析树、抽象语法树

语法错误可能有:

  1. 关键字、标识符拼写错误,如intege
  2. 语法结构出错,少分号,begin/end不匹配
  3. 静态语义错误:类型不一致、参数不匹配,如需要传入double却传入了char
  4. 动态语义错误:无穷递归(这就是为什么有的编译器会报错whille(t){}里没有t–,即使你在块里写了它也找不到,因为它只看一开始有没有,很傻)、0作除数

处理目标:

  1. 正确报告错误以及地点(有一些明明是x行错误,却报y行错误)
  2. 迅速恢复(要么一直找,直到找到想要的记号,这样比较愚蠢;要么替换纠正,容易改不对,引发更多报错)
  3. 不影响源程序的分析速度

词法分析可以合并到文法当中

上下文无关文法 CFG: Context-Free Grammar

文法是对语言结构的定义和描述。即从形式上用于表述和规定语言的结构称为“文法”。如:“草吃羊”在文法上正确,但语义不正确。

定义

G=(T,N,P,S)
T 是终极符号集合,可以理解为token,一般用小写、黑体表示
N是非终结符号集合,一般用大写、小写斜体表示
P是产生式或文法规则A->a集合,其中A是非终结符,a是(TUN)*,如<句子>-><主语><宾语>
S是唯一的开始符号,是非终结符

另外,小写希腊字符表示文法符号串(可为空)

暂时没有自动生成上下文无关文法的工具,必须手动写

推导方法

给定文法G,从G的开始符号S开始推导,不断用相应规则的右部来替代规则的左部,每次仅用一条规则去推导,直到所有的非终结符都被终结符号替代为止。
依据上述过程,最终的串称为句子,所有句子的集合称为语言。定义如下: L ( G ) = { s ∣ S = > ∗ s } L(G)=\{s|S=>*s\} L(G)={sS=>s},其中,推出符号*表示经过0或多步推导出。

最左推导和最右推导

有若干语法成分同时存在时,总是从最左的语法成分进行推导,这称之为最左推导。(同理可以定义最右推导)
例:
文法:

exp->exp op exp|(exp)|**number**
op->+|-|\*

需要分析的字符串:(34-3)*42
推导过程:

exp=>exp op exp
=>exp op number
=>exp * number
=> (exp)*number
=>(exp op exp)*number
=>(exp op number)*number
=>(exp - number) * number
=>(number - number ) * number

上例是最右推导,最左推导如下:

(1) exp => exp op exp [exp -> exp op exp]
(2) => (exp) op exp [exp -> (exp)]
(3) => (exp op exp) op exp [exp -> exp op exp]
(4) => (number op exp) op exp [exp -> number]
(5) => (number - exp) op exp [ op -> - ]
(6) => (number - number) op exp [exp -> number]
(7) => (number - number) * exp [ op -> *]
(8) => (number - number) * number [exp -> number]

上下文无关文法举例:

  1. 文法G:E->(E)|a, 文法定义的语言是:L(G)={a,(a),((a)),…}={ ( n a ) n ∣ (^na)^n| (na)nn是>=0的整数}
  2. G:E->E+a|a,文法定义的语言是:L(G)={a,a+a,a+a+a,…}
  3. 正则式:a+,文法是G:A->Aa|a或者A->aA|a,语言是L(G)={ a n a^n an,n是>=1的整数}
  4. 正则式:a*,文法是G:A->Aa|ε或者A->aA|ε,语言是L(G)={ a n a^n an,n是>=0的整数}
  5. 文法G:E->(E) ,语言是L(G)={},没有终结符,没有句子,无限递归

分析树

以上例为例,
在这里插入图片描述
看序号可知是最左推导,与前序编号对应
下面是最右推导,与后序遍历对应
在这里插入图片描述

①父节点和子结点之间构成了一条文法规则
②叶节点都是终结符号,内部结点都是非终结符号

每个分析树只有唯一的一个最左推导和一个最右推导

分析树->抽象语法树

在这里插入图片描述
分析树复杂,但信息丰富,而抽象语法树简洁、抽象,用于语义分析

常见的上下文无关文法

  1. 算数表达式
exp->exp op exp|(exp)|**number**
op->+|-|\*
  1. if-else
    下面这个文法,有重叠的部分,比较低效
G: statement -> if-stmt | other
if-stmt -> if ( exp ) statement |if ( exp ) statement else statement
exp -> 0 | 1

下面这个文法是有歧义的文法

G: statement -> if-stmt | other
if-stmt -> if ( exp ) statement else-part
else-part -> else statement | ε
exp -> 0 | 1
  1. 括号匹配文法
G: A ->(A)A|ε
  1. 带分号的文法
    这个文法是错的,他的缺点是最后一个语句没有结束的分号
stmt-sequence -> stmt ; stmt-sequence | stmt
stmt -> s

下面这个文法可以,但是会推导出空语句:L(G’)= {ε, s;, s;s;, s;s;s;,…}

stmt-sequence -> stmt ; stmt-sequence | stmt | ε
stmt -> s

下面这个也是

stmt-sequence -> stmt-other1 stmt-other2
stmt-other1 -> stmt | ε
stmt-other2 -> ;stmt stmt-other2 | ;
stmt -> s

文法设计

二义性文法

可生成两个不同分析树的串的文法叫二义性文法。
34-3*42的分析树与语法树可以有两种。
算术表达式文法存在二义性的根源是什么?有2条规则都能往下推导,没有考虑优先级。

消除二义性的方法:
1.不修改文法,指定正确的分析树(只需手动修改生成的代码)LL分析表有冲突时选择其中一条
2.修改文法,会改得很乱

1.算术表达式修改文法,确定乘法优先级于加法;

exp -> exp addop exp | term
addop -> + | -
term -> term mulop term| factor
mulop -> *
factor -> (exp) | number

在此基础上,确定乘法和加法都是左结合的

exp -> exp addop term | term
addop -> + | -
term -> term mulop factor | factor
mulop -> *
factor -> (exp) | number

下面这个文法的缺陷是,输入者必须输入带括号的表达式

exp  factor op factor | factor
factor  (exp) | number
op  + || *

2.if-else,悬挂的else问题在这里插入代码片
最近嵌套规则用于解决悬挂else问题

statement  matched-stmt | unmatched-stmt
matched-stmt  if ( exp ) matched-stmt else matched-stmt | other
unmatched-stmt  if ( exp ) statement | if ( exp ) matched-stmt else unmatched-stmt
exp  0 | 1

下面的文法是强制if加上end

if-stmt -> if condition then statement-sequence end if |if condition then statement-sequence else statement-sequence end if

3.无关紧要的二义性文法:分号结尾的语句

stmt-sequence -> stmt-sequence ; stmt-sequence | stmt
stmt -> s

扩展巴科斯范式:EBNF extended Backus Normal Form

{}表示重复
在这里插入图片描述

用[]表示可选,比如:

G: statement  if-stmt | other
if-stmt  if ( exp ) statement [ else statement ]
exp  0 | 1

文法和语言分类

0型、1型、2型、3型(乔姆斯基层次)

-产生式左部范围右部范围左部右部备注
0型u->v(TUN)+(TUN)*>=1>=0可计算枚举语言,短语结构文法
1型xUy->xuy(TUN)+(TUN)*>=1>=0上下文相关文法
2型U->vN(TUN)*=1>=0上下文无关文法
3型U->t,U->WtNTUN=1<=2正则文法、线性文法

越往高级,限制越多,高级的语言符合低级

(左线性) P:U -> t 或 U -> Wt 其中 U、W∈N t∈T
(右线性) P:U -> t 或 U -> tW 其中 U、W∈N t∈T
在这里插入图片描述
这一分类的研究意义在于模型的可解释性,但是它的描述能力较弱

相关术语

直接推导

在这里插入图片描述

+推导

在这里插入图片描述
等于说是通过一步或多步推导出来的

*推导

在这里插入图片描述
通过0步或多步推导

句型、句子、语言

文法G = (T, N, P, S),S =>* a ,
如果a是(TUN)*,那么a是句型 (存在非终结符号。即分析树的剖面);
如果a是T*,那么a是句子(全部是终结符号,也就是叶子节点)(句子属于句型)
文法G产生的语言是L(G[S]) = {a | S =>* a ,a ∈ T* }

短语、简单短语、句柄

G = (T, N, P, S),w = xuy ∈ (TUN)* 为文法的句型
若:S => xUy ,且 U =>+ u,则u是句型w相对于U的短语(子树)
若:S => xUy, 且 U => u,则u是句型w相对于U 的简单短语;
U是非终结符,u是一步或多步的TUN,xy都是0步或多步的TUN
任一句型的最左简单短语称为该句型的句柄。

例子:
在这里插入图片描述
黄色是剖面(句型)
在这里插入图片描述
可以发现,这种话的说法就是“u是句型xx相对于U的短语”,其中句型是固定的,因为这是题里给的,u是黄色的结点,U是他们规约以后蓝色的结点
在这里插入图片描述
简单短语也就是一步可以得出的,最左的简单短语是句柄

正则文法

在这里插入图片描述

DFA->正则文法

在这里插入图片描述
例子:

在这里插入图片描述
普通的转移直接把吃进的字符和到达的状态连起来当右部,出发点当左部;可接收的状态再加一条可以推出epsilon

正则文法->DFA

在这里插入图片描述在这里插入图片描述
先按反过程画出来DFA,画完所有的以后加一个终态Z,把那些可以推出epsilon的状态都指向Z,把那些直接推出终结符的语句,也指向Z

DFA->正则表达式

在这里插入图片描述在这里插入图片描述
将词法分析的流程反向做一遍
在这里插入图片描述
r = ( a ∣ b ) ∗ ( a a ∣ b b ) ( a ∣ b ) ∗ r = (a|b)*(aa|bb)(a|b)* r=(ab)(aabb)(ab)

正则文法->正则表达式

在这里插入图片描述
递归即闭包
在这里插入图片描述

正则表达式->正则文法

上表反过来
在这里插入图片描述
在这里插入图片描述

自顶向下分析方法(LL分析算法) top-down parsing

从文法G的开始符号S开始推导得出句子t,遍历所有t,如果t==给定的句子s,那么s可以由G推导出来。

回溯分析方法backtracking parser

思想就是往下推导,如果不匹配就回溯,效率非常低,不聪明。没人用。
在这里插入图片描述

tokens[ ]; /* 词法分析得到的单词列表 */
int i = 0;
stack = [S]; /* 栈内放文法的开始符号 */
while ( stack != [] )
	if (stack[top] 是终结符号 t )
		if ( t == tokens[i] ) { i++; pop(); }
		else { backtrack( ) }
	else if (stack[top] 非终结符号 T )
		pop( ); push( 关于非终结符号T的下一条规则的右部 )

在这里插入图片描述
在这里插入图片描述
压栈的时候,是从右往左压,一旦栈顶是终结符就去匹配:不匹配就回溯,匹配就消掉

预测分析方法 predictive paeser

递归下降分析 recursive-descent parsing

概念:将一个非终结符A的文法规则看作将识别A的一个过程的定义。
基本思想(或核心过程):

  1. 为每一个非终结符号A构造一个分析函数。即:非终结符A的文法规则的右部指出此过程的代码结构。
  2. 用前看符号指导产生式规则的选择。

特点:方便手工编码实现;错误定位和诊断信息准确
具有隐式栈(函数调用),前看(if)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例子:exp ->exp addop term | term具有左递归,如果不消除,会产生死循环如下:

Parse_Exp( )
	Parse_Exp( ); 
	……

消除左递归,变成:if-stmt -> term | {addop term},可以写出代码:
在这里插入图片描述
对于文法规则 A − > β 1 β 2... β n A ->\beta 1\beta 2...\beta n A>β1β2...βn,对于每个 β i \beta i βi ,

若为非终结符号:递归调用
若为终结符号:判断是否匹配
	 匹 配:继续读入token分析
	 不匹配:报告错误。

LL(1)分析法 ll(1) parsing (重点)

流程:上下文无关文法CFG->FIRST,FOLLOW->分析表->语法分析器

LL(1)的内涵:
第一个L:算法从左向右处理token
第二个L:最左推导
1:前看一个符号

思想:
用显式栈完成分析(分析栈)
表驱动的分析算法(分析表):对于每个前看符号,有至多一个产生式与之对应

分析表->语法分析

一手拿串(最后加上一个$作末尾符),一手拿栈(栈里最开始有开始符号S),看着表,类似回溯分析的方法但不需要回溯,下面举几个例子:
在这里插入图片描述
考试时用下面的格式写:
在这里插入图片描述
注意 1写上编号 2多个匹配要分开匹配 3 分析栈可以从右往左(头对头),也可以从左往右写,看自己习惯 4动作只有生成式、匹配、接受
在这里插入图片描述
代码:
在这里插入图片描述

分析表的构造(两条规则)

在这里插入图片描述
规则1:如果 A − > α A->\alpha A>α是一个产生式,且有 α = > ∗ a β \alpha =>*a\beta α=>aβ成立,其中a是一个记号,则将 A − > α A->\alpha A>α添加到表格M(A,a)中(通过0步或多步,A可以推出以a开头的串)
规则2:如果 A − > α A->\alpha A>α是一个产生式,且有 α = > ∗ ϵ , S \alpha =>*\epsilon,S α=>ϵ,S $ =>* β A a γ \beta Aa\gamma βA 成立,其中S是文法的开始符号,a是一个记号(或者$),则将 A − > α A->\alpha A>α添加到表格M(A,a)中(也就是A产生了 α \alpha α,最终推出了空串,但是前看符号没有空的,就去看A之后的第一个符号,而S最终推出了 β A a γ \beta Aa\gamma βA,这里面A的下一个终结符就是a,那么就用a来前看)
在这里插入图片描述
这两个规则虽好,但推导起来不容易,因此有了first和follow集的算法

如果文法G相关的LL(1)分析表的每一个项目中至多只有一个产生式,则该文法称为LL(1)文法。(一条或者没有,才叫ll(1),否则就不叫)
①如果分析表中不包含任何产生式,该项目有什么意义?文法描述能力不足或者语句出错
②分析表中包含2个或者2个以上的产生式,有什么方法可以解决?冲突的解决

拿之前的if-else的二义性文法举例,
在这里插入图片描述
在这里插入图片描述
出现了冲突,直接去掉第二条,实现“最近嵌套”
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

消除左递归、提取左公因子

①消除左递归,如exp -> exp addop term | term易出现死循环
把左递归变成右递归,方法如下:
在这里插入图片描述
原本的|后面的终结符,放到前面(因为这是左递归结束时的样子,这个文法可以表示 β α α α α α . . . . . . \beta \alpha \alpha \alpha \alpha \alpha...... βααααα......),把含有递归的部分用新字母替换;新字母产生 α \alpha α和它自己,也就是右递归,最后需要用epsilon来结束递归。
这种消除左递归方法没有改变文法,但是改变了分析树。
例子:
在这里插入图片描述

更一般的形式
在这里插入图片描述
可以提取A的公因子出来,因此和上述的方法是一致的,只是更符合一般情况。

在这里插入图片描述
隐式的、间接的左递归
例如: A − > B α , B − > A β ∣ γ A->B\alpha ,B->A\beta |\gamma A>Bα,B>Aβγ,经过代入后,可得 A − > A β α ∣ γ α A->A\beta \alpha |\gamma \alpha A>Aβαγα,又出现了左递归。
消除方法:
在这里插入图片描述
在这里插入图片描述
i=1,只执行remove immediate left recursion involving A1
在这里插入图片描述
i=2,j=1,将A2->A1 β \beta β用A2-> α 1 β 1 \alpha1 \beta 1 α1β1替换,其中 A 1 − > α 1 A1->\alpha 1 A1>α1,也就是把消除了左递归的A直接代入到B里,继续执行外循环,消除A2的直接左递归
在这里插入图片描述

②提取左因子,如:if-stmt -> if ( exp ) statement |if ( exp ) statement else statement 易造成回溯
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

First集合,Follow集合 (重点)

这两个集合是对两个推导规则的算法化
左递归不影响算first,follow集

First集合=头符号集合 定义:

定义1: F I R S T ( α ) = { a ∣ α = > ∗ a β , a ∈ T , α , β ∈ ( T U N ) ∗ } FIRST(\alpha )=\{a|\alpha =>*a\beta ,a\in T,\alpha ,\beta \in (T U N)*\} FIRST(α)={aα=>aβ,aT,α,β(TUN)},如果alpha可以推出头符号a,那么a就是它的first集里的元素。但是这种其实就是推导规则1,也没法算,所以还是要看定义2.

定义2:令X为一个文法符号(一个终结符或非终结符)或ε,则集合First(X)由终结符或者ε组成,它的定义如下:
1.若X是终结符或ε,则FIRST(X) = {X},即它的first集只有它自己(终结符或空)
2.若X是非终结符,则对于每个产生式X→X1X2…Xn,First(X)都包含了FIRST(X1)-{ε}。 (先包含上X1的fist集)
①若对于某个i<n,所有的集合FIRST(X1),FIRST(X2),…, FIRST(Xi)都包括了ε,则FIRST(X)也包括了FIRST(Xi+1)-{ε} (如果前面都有空,那么把Xi+1的first集也包含进来)
②若所有的集合FIRST(X1), FIRST(X2),…, FIRST(Xn)都包括了ε,则FIRST(X)也包括ε。(当所有都有epsilon,First(x)才能包括epsilon)
在这里插入图片描述

性质:
当存在一个推导A =>* ε,则非终结符A称作可空的(nullable)。
当且仅当FIRST(A)包含ε时,非终结符A为可空的。

算法:
在这里插入图片描述
例子:
在这里插入图片描述
在这里插入图片描述
一开始,所有的非终结符的first集都为空(我们也主要是算非终结符,因为终结符就是它自己本身)。然后通过一遍遍的扫描,对first集进行增加,直到first集不再变化为止。
此处需要注意:空集{}和epsilon是不同的
在这里插入图片描述
在这里插入图片描述
考试时可以在草稿纸上演算,不需要写得这么详细,直接出最后结果,因为这只是大题中的一小步
在这里插入图片描述在这里插入图片描述

Follow集合=后继符号集合定义:
定义1:FOLLOW(A) ={a | S =>* μAβ,且a∈T,a∈FIRST(β),S是开始符号,μ∈T*,A∈N,β∈V+}
若S =>* μAβ,且β =>* ε,则$ ∈ \in FOLLOW(A)。
这对应的是推导规则2.

定义2:给出一个非终结符A,那么集合FOLLOW(A)则是由终结符或$组成,集合FOLLOW(A)的定义如下
① 若A是开始符号,则$就在FOLLOW(A)中。
② 若存在产生式B→αAγ,则FIRST(γ)-{ε}在FOLLOW(A)中。
③ 若存在产生式B→αAγ,且ε在FIRST(γ)中,则FOLLOW(A)包括FOLLOW(B)。(因为gamma可以为空,所以A的后继包括B的后继)
在这里插入图片描述
只需要考虑非终结符的follow集,对于每条产生式,拿到非终结符后面的first集,如果这一集合为空,那么再加上左部的follow集。
求follow,需要先求出first,也是走几遍直到没有新的变化为止:
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述

流程就是:
分析右部的非终结符,如果它是开始符,就加上$,然后加上后面一个的first集,如果后面没东西或者后面的first集里有epsilon,就加上左部的follow集
做熟练了你能直接看出来,都不用一遍一遍去搞

LL(1)文法分析表的构建算法
重复以下两个步骤针对任意非终结符A以及产生式规则A-> α \alpha α

  1. 对于First( α \alpha α)中的每个记号a,都将A-> α \alpha α添加到项目M(A,a)中
  2. 若ε在First ( α \alpha α)中,则对于Follow(A)中的每个元素a( a是一个记号或者$ ),则将 A-> α \alpha α添加到项目M(A,a)中。

若满足以下条件,则BNF中的文法就是LL(1)文法:

  1. 在每个产生式A→α1|α2|…|αn,对于所有的i和j,满足1<=i, j<=n,且i ≠ j,FIRST(αi)∩FIRST(αj)为空。 (如果存在交集,那么表里一个格子会填两个产生式,发生冲突)
  2. 若对于每个非终结符A都有FIRST(A)包含了ε,那么
    FIRST(A)∩FOLLOW(A)为空。(如果前看里包含epsilon,那么前看和后继就不能有交集,否则也会出现一个格子里填两条)
    在这里插入图片描述在这里插入图片描述在这里插入图片描述

总结,LL(1)分析的流程:
①改写文法,必须消除左递归,提取左公因子
②构造分析表(first,follow)
③解决冲突
④开始分析,串|栈|动作

自底向上分析方法(LR分析算法)bottom-up parsing

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值