编译原理 上下文无关文法

1.定义

术语

术语 含义
非终结符 语句或者表达式
终结符 关键字或者符号,也称为记号
产生式 由左部和右部组合而成
记号串 零个或多个记号的序列。
空串 包含零个记号串的记号串

上下文无关文法包含如下四个部分;

  1. 一个记号集合,称为终结符号
  2. 一个非终结符集合
  3. 一个产生式集合。每个产生式具有一个左部和一个右部,左部和右部由箭头连接,左部是一个非终结符。右部是记号和(或)非终结符序列
  4. 一个开始符号。开始符号是一个指定的非终结符

上下文无关文法的作用是:描述语言的语法。
例子2.1:描述算术表达式语法的产生式:

list -> list + digit
list -> list - digit
list -> digit
digit -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 

这里,list,digit都是非终结符。终结符号为数字(0,1,2…)和运算符(+,-)
左部的非终结符皆为list(列表)的三个产生式的右部可以合并成:

list -> list + digit | list - digit | digit

开始非终结符:list。

如果一个非终结符出现在一个产生式的左部,那么,该产生式称为该非终结符的产生式。
记号串:零个或多个记号的序列。

定理1:从开始符号出发,反复替代产生式中的非终结符(用该非终结符的产生式的右部),一个文法可以产生一个串。由一个文法的开始符号产生的记号串形成了该文法定义的语言

分析树:树中的每个节点是一个文法符号。一个内节点和它的所有子节点对应一个产生式。内节点对应产生式的左部,子节点对应产生式的右部。

例子2.3 Pascal语言的begin-end语句块是由分号分隔的语句序列。它的部分文法如下:

block -> begin opt_stmts end
opt_stmts -> stmt_list | e
stmt_list -> stmt_list ; stmt | stmt

目前,我们还没有定义语句stmt的产生式。

定义2:分析树
分析树描绘了如何从文法的开始符号开始推导出它的语言中的一个语句。给定一个上下文无关文法,分析树是具有如下特性的树:

  1. 树根标记为开始符号。
  2. 每个叶节点由记号或者e标记。
  3. 每个内节点由一个非终结符标记。
  4. 如果A是某个内节点的非终结符号标记,X 1到n是该节点从左到右排列的所有子节点的标记,那么A->X1X2…Xn是一个产生式。这里,X1到Xn是一个终结符或非终结符。

定理3:结果
一棵分析树从左到右的叶节点是这棵分析树生成的结果。分析树生成的结果是由根节点的非终结符生成或导出的

定义3:语法分析
一个文法生成的语言是它的某个分析树生成的串的集合。为给定的记号串找到一个分析树的过程称为这个串的语法分析(parsing)。

定义4:二义性
一棵分析树读完它的叶节点只能生成惟一的一个串,但是,一个文法可能有多棵分析树生成相同的记号串。这样的文法称为具有二义性的文法。

如果存在一个具有多棵分析树的记号串。那么就是有二义性,构造程序设计语言及其编译器的时候,要设计无二义性的文法。

操作符的结合规则
右结合的操作符,可以由如下文法产生:

right -> letter = right | letter
letter -> a | b | ... | z

操作符的优先级
优先级可能会导致二义性问题,这时候就要写正确的文法。

为了构造表达式的文法,需要根据操作符的结合性和优先级来构建。

定义5:优先级表
在优先级表中,操作符按照优先级递增的次序排列,相同优先级的操作符在同一行。

因为当前的算术表达式有两个优先级层次(加减和乘除),因此我们用两个非终结符(expr和term)表示两个不同的优先级层次,使用另一个非终结符factor产生表达式的基本单元。表达式的基本单元是数字和带括号的表达式。factor的产生式如下:

factor -> digit | (expr)

现在考虑具有最高优先级的二元操作符*和/。这些操作符是左结合,可以写出文法:

term -> term * factor
      | term / factor
      | factor

类似的expr的生成式:

expr -> expr + term
      | expr - term
      | term

算术表达式的最终文法:

expr -> expr + term | expr - term | term
term -> term * factor | term / factor | factor
factor -> digit | (expr)

上面的文法可以这样解释:一个表达式是由+号和-号分隔的term表,而term是有*号或/号分隔的factor表。任何带括号的一个表达式都是一个factor。

2.3.1 后缀表示

定义6:后缀表示
归纳定义:

  1. 如果E是一个变量或者常量,则E的后缀表示是E本身。
  2. 如果E是形如E1 op E2的表达式,其中op是一个二元操作符,则E的后缀表达式是E1’E2’op,这里E1’和E2’分别是E1和E2的后缀表示。
  3. 如果E是形如(E1)的表达式,则E1的后缀表示是E的后缀表示。

后缀表达式特点:不需要括号。这是因为一个表达式的操作符的位置和每个操作符的个数(参数数量)只允许后缀表达式的一种解码方式。

2.3.3 语法制导定义

语法制导定义使用上下文无关文法来说明输入的语法结构。它通过每个文法符号和一个属性集合相关联,通过每一个产生式和一个语意规则集合相关联。语义规则用来计算与产生式中出现的符号相关联的属性的值。文法和语义规则集合构成了语法制导定义。

定义7:注释分析树
假定分析树的节点n用文法符号X标识。我们用X.a表示节点n上X的属性a的值。节点n上X.a的值是使用与X产生式相关联的属性a的语义规则来计算的。每个节点都具有属性值的分析树称为注释分析树。

定义8:综合属性
如果分析树的某个节点的属性值是由其他子节点的属性值确定的,则我们称该属性为综合属性。

由上述定义可知:一棵分析树的所有综合属性值的计算只需要分析树的一次自底向上遍历。

2.3.4 深度优先遍历

计算语义规则时如何访问分析树

procedure visit(n:node)
begin
	for n的每个子节点m,从左到右do
		visit(m);
		计算节点n处的语义规则
end

2.3.5 翻译模式

定义9:翻译模式
一个翻译模式是一个上下文无关文法,其中被称为语义动作的程序段被嵌入到产生式右部。

2.3.6 翻译的输出

翻译模式的语义动作把翻译的输出以一次一个字符或一个字符串的形式写入一个文件。

语义制导定义具有如下特性:
定理4:语义制导定义的特性
每个产生式左部的非终结符的翻译是将该产生式右部的非终结符的翻译按照它们在右部出现的次序连接得到的,在连接的过程中可能还需要附加(也可能不需要)一些额外的串。

定义10:简单的语义制导定义

2.4 语法分析

定义11:语法分析
决定一个记号是否能够由一个文法产生的过程。语法分析器应该具有构造分析树的能力,否则,不能保证翻译的正确性。

程序设计语言语法分析器总是从左到右扫描输入,每次超前扫描一个记号。

语法分析方法:

2.4.1 自顶向下语法分析

自顶向下方法:
按照从根节点到叶节点的顺序构造分析树,反之则是自底向上的文法。

这里我们选一个适用于采用自顶向下语法分析的文法。下面的文法生成一个Pascal的类型子集。

type -> simple
	 | id
	 | array[simple] of type
simple -> integer
	    | char
	    num dotdot num

如何自顶向下地构造一个分析树?
从标有非终结符的根节点开始,反复执行下面两步:

  1. 在标有非终结符A的节点n,选择A的一个产生式,用该产生式右部的符号构造节点n的子节点。
  2. 寻找下一个要构造子树的节点。

上面描述的方法,为非终结符选择产生式的过程可能会涉及“试验和错误”的问题,即在选择一个产生式的时候,如果一个产生式不合适我们不得不回溯,选择另一个产生式,一个产生式不合适是指使用了该产生式之后无法 产生与输入串匹配的分析树。
预测分析法可以避免回溯。

2.4.2 预测分析法

递归下降分析法:一种自顶向下的分析方法,在这种方法中,我们执行一组递归过程来处理输入串。每个过程都唯一地与文法的一个非终结符相关联。
预测分析法:是一种特殊的递归下降分析法。
在预测分析法中,超前扫描符号无二义地确定了为每个非终结符选择的过程。处理输入时调用的过程序列隐式地定义了输入串的分析树。

procedure match(t:token);
begin
	if lookahead = t then
		lookahend := nextoken
	else error
end;

procedure type;
begin
	if lookahead is in {integer,char,num} then
		simple
	else if lookahead = '|' then begin
		match('|');match(id)
	end
	else if lookahead = array then begin
		match(array);match('[');simple;match(']');match(of);type
	end
	else error
end;

procedure simple:
begin
	if lookahead = integer then
		match(integer)
	else if lookahead == char then
		match(num);match(dotdot);match(num)
	end
	else error
end;

2.4.3 何时使用e产生式(空产生式)

当没有其他产生式可用的时候,递归下降语法分析器把e产生式作为默认产生式使用。
FIRST()函数用于计算非终结符的可选头部

2.4.4 设计一个预测语法分析器

定义12:预测语法分析器
是一个由多个过程组成的程序,每个过程对应一个非终结符。每个过程完成如下两项任务:

  1. 检查超前扫描符号,决定使用那个产生式。如果超前扫描符号在DIRST(@)中,则选择使用右部为@的产生式。对于任何超前扫描符号,如果产生式右部存在冲突,那么我们不能在这种文法上使用这种分析方式。如果超前扫描符号不在任何其他右部的FIRST集合中,右部具有e的产生式将被使用。
  2. 过程通过模仿其右部来使用一个产生式。一个非终结符导致该非终结符对应的过程被调用。一个与超前扫描符号匹配的记号导致下一个输入记号被读入。如果在某个点上,产生式的记号与超前扫描符号不匹配,则报告出错。

定义13: FIRST
令a是非终结符A的某产生式的右部。定义FIRST(a)是作为由a产生的一个或多个串的第一个符号出现的集合。

由于本章实现的翻译模式不涉及非终结符的属性,下面仅给出满足当前要求的构造方法:

  1. 构造一个预测语法分析器,忽略产生式中的语义动作。
  2. 把翻译模式中的语义动作拷贝到语法分析器。如果一个语义动作出现在产生式p的文法符号的后面,则该语义动作被拷贝到实现X的代码后面,否则,如果语义动作出现在一个产生式的开始,则该语义动作被拷贝到该产生式的实现代码的最前面。

2.4.5 左递归

递归下降语法分析器很可能造成无限循环。当出现下面这样一个左递归产生式时,无限循环就会出现:

expr -> expr + term

因为只有右部终结符与超前扫描符号匹配时,超前扫描符号才会发生改变。二expr是非终结符,输入符号在递归调用期间没有机会改变,导致无线循环。

通过重写与递归相关的产生式,我们可以消除左递归产生式。考虑非终结符A的两个产生式:

A -> Aa | b

这里a和b是不以A开始的终结符和非终结符序列。例如,在产生式

expr -> expr + term | term

中,A=expr,a = +term,b=term
上面的产生式可以通过如下方式改写得到:

A -> bR
R -> aR | e

这里R是一个新的非终结符。产生式R->aR以R自身作为产生式右部最后一个符号,因而是右递归的。右递归产生式导致向右下侧延伸的分析树,这使得包含左结合操作符表达式的翻译变得十分困难。

2.5 简单表达式的翻译器

实现翻译的定义。

expr -> expr + term {print('+')}
expr -> expr - term {print('-')}
expr -> term
term -> 0			{print('0')}
term -> 1			{print('1')}
...
term -> 9			{print('9')}

这个文法是左递归的,前两行就可以看出,因此,需要能够被预测语法分析器分析之前需要加以修改。消除左递归得到适用于预测递归下降编译器的文法。

2.5.1 抽象语法和具体语法

定义14 抽象语法树(简称语法树):每个节点表示一个操作符,该节点的子节点表示操作数。
具体语法树:就是分析树,其相应的文法称为具体语法。
消除了左递归的文法有时候不一定能满足需要,例如:
文法:

expr -> trem rest
rest -> + expr | -expr | e
term -> 0 | 1 | ... | 9

2.5.2 调整翻译模式

将产生式

A -> Aa|Ab|y

转换成

A -> yR
R -> aR | bR | e

将上述转换策略应用到后缀生成中的产生式,那么,可以得到如下文法:

expr -> term rest
rest -> + term {print('+')} rest | -term {print('-')} rest | e
term -> 0 {print('0')}
term -> 1 {print('1')}
...
term -> 9 {print('9')}

这个文法可以得到我们预期的翻译。

2.5.3 非终结符expr、term和rest的过程

利用c语言实现上述翻译器。

expr()
{
	term();rest();
}
rest() 
{
	if (lookahead == '+') {
		match('+'); term(); putchar('+'); rest();
	}
	else if (lookahead == '-') {
		match('-');term();putchar('-');rest();
	}
	else;
}
term()
{
	if (isdigit(loojahead)){
		putchar(lookahead);match(lookahead);//这里由于match会改变超前扫描符号,所以需要先输出
	} else
	 error();
}

2.5.4 翻译器的优化

定义15 尾递归:如果一个过程中执行的最后一条语句是对该过程的递归调用,则称该调用为尾递归的。上面的rest函数就是尾递归的。
用循环代替尾递归

rest()
{
L:	if(lookahead == '+') {
		match('+'); term(); putchar('+');goto L;
	else if(lookahead == '-') {
		match('-'); term(); putchar('-');goto L;
	}
	else ;
	}
}

下面将expr和rest合二为一

expr()
{
	term();
	while(1) 
		if (lookahead == '+') {
			match('+');term();putchar('+');
		}
		else if (lookahead == '-') {
			match('-');term();putchar('-');
		}
		else break;
}

你可能感兴趣的:(linux)