Lex & Yacc 学习笔记(4)- Yacc语法结构

一、背景

  • 概念
    yacc(Yet Another Compiler Compiler),是Unix/Linux上一个用来生成编译器的编译器(编译器代码生成器).
    使用巴克斯范式(BNF)定义语法,能处理上下文无关文法(context-free)。出现在每个产生式左边(left-hand side:lhs)的符号是非终端符号,出现在产生式右边(right-hand side:rhs)的符号有非终端符号和终端符号,但终端符号只出现在右端。
    yacc是开发编译器的一个有用的工具,采用LR(1)(实际上是LALR(1))语法分析方法。
    LR(k)分析方法是1965年Knuth提出的,括号中的k(k >=0)表示向右查看输入串符号的个数。LR分析法正视给出一种能根据当前分析栈中的符号串和向右顺序查看输入串的k个符号就可唯一确定分析器的动作是移进还是规约和用哪个产生式规约。
    这种方法具有分析速度快,能准确,即使地指出出错的位置,它的主要缺点是对于一个使用语言文法的分析器的构造工作量相当大,k愈大构造愈复杂,实现比较困难。

一个LR分析器有3个部分组成:

  • 总控程序,也可以称为驱动程序。
    对所有的LR分析器总控程序都是相同的。
  • 分析表或分析函数。
    不同的文法分析表将不同,同一个文法采用的LR分析器不同时,分析表也不同,分析表又可分为动作(ACTION)表和状态转换(GOTO)表两个部分,它们都可用二维数组表示。
  • 分析栈,包括文法符号栈和相应的状态栈。
    它们均是先进后出栈。 分析器的动作由栈顶状态和当前输入符号所决定(LR(0)分析器不需要向前查看输入符号)。
    LR分析器工作过程如下 :
    其中SP为栈指针,S[i]为状态栈,X[i]为文法符号栈。状态转换表内容按关系GOTO[Si,X] = Sj确定,该关系式是指当栈顶状态为Si遇到当前文法符号为X时应转向状态Sj。X为终结符或非终结符。 ACTION[Si,a]规定了栈顶状态为Si是遇到输入符号a应执行的动作。

本文讨论yacc语法的格式并描述可用的各种特征和选项

二、yacc语法结构

yacc语法包括三部分:定义段、规则段和用户子例程段

...定义段...

%%

...规则段...

%%

...用户子例程段...

各部分由以两个百分号开头的行分开,尽管某一个部分可以为空,但是前两部分是必须的,第三部分和前面的百分号可以省略。

符号

yacc 语法由符号组成,即语法的“词”。符号是一串不以数字开头的字母、数字、句点和下划线。符号error专用于错误恢复,另外,yacc对任何符号都不会附加“先验”的意义。

由词法分析程序产生的符号叫做终结符号或者标记。定义在规则左侧的叫做非终结符号或者非终结。标记也可能是字面上引用的字符,通常遵循约定:标记大写,非终结符号小写

定义段

定义段包括文字块,逐字拷贝到生成的C文件开头部分的C代码,通常包括声明和#include行。可能有%union %start %token %type %left %right 和 %nonassoc声明。

也可以包含普通的C语言风格的注释,所有这些都是可选的,在简单的语法分析程序中,定义段可能完全是空的。

如在定义部分定义标志:

%token INTEGER

当运行yacc后,会产生头文件,里面包含该标志的预定义,如:

#ifndef YYSTYPE 
#define YYSTYPE int 
#endif 
#define INTEGER 258 
extern YYSTYPE yylval;

lex使用该头文件中的标志定义。Yacc调用lex的yylex()来获得标志(token),与标志对应的值由lex放在变量yylval中。yylval的类型由YYSTYPE决定,YYSTYPE缺省类型是int。如:

[0-9]+ { 
yylval = atoi(yytext); 
return INTEGER; 
}

标志0-255被保留作为字符值,一般产生的token标志从258开始。如:

[-+] return *yytext; /* return operator */

返回加号或减号。注意要把减号放在前面,避免被认作是范围符号。
对于操作符,可以定义%left和%right:%left表示左相关(left-associative),%right表示右相关(right-associative)。可以定义多组%left或%right,在后面定义的组有更高的优先级。如:

%left ‘+’ ‘-‘
%left ‘*’ ‘/’

上面定义的乘法和除法比加法和减法有更高的优先级。
改变YYSTYPE的类型。如这样定义TTSTYPE:

%union
{ 
     int iValue; /* integer value */ 
     char sIndex; /* symbol table index */ 
     nodeType *nPtr; /* node pointer */ 
};

则生成的头文件中的内容是:

typedef union
{ 
     int iValue;      /* integer value */ 
     char sIndex;    /* symbol table index */ 
     nodeType *nPtr; /* node pointer */ 
} YYSTYPE; 
extern YYSTYPE yylval;

可以把标志(token)绑定到YYSTYPE的某个域。如:

%token <iValue> INTEGER 
%type <nPtr> expr

把expr绑定到nPtr,把INTEGER绑定到iValue。yacc处理时会做转换。如:

expr: INTEGER { $$ = con($1); }

转换结果为:

yylval.nPtr = con(yyvsp[0].iValue);

其中yyvsp[0]是值栈(value stack)当前的头部。

定义一元减号符有更高的优先级的方法:

%left GE LE EQ NE '>' '<' 
%left '+' '-' 
%left '*' 
%nonassoc UMINUS

%nonassoc的含义是没有结合性。它一般与%prec结合使用表示该操作有同样的优先级。如:

expr: '-' expr %prec UMINUS { $$ = node(UMINUS, 1, $2); }

表示该操作的优先级与UMINUS相同,在上面的定义中,UMINUS的优先级高于其他操作符,所以该操作的优先级也高于其他操作符计算。

规则段

规则段由语法规则和包括C代码的动作组成。

规则中目标或非终端符放在左边,后跟一个冒号(:),然后是产生式的右边,之后是对应的动作(用{}包含)。如:

%token INTEGER
%%
program: program expr '\n' { printf("%d\n", $2); } 
              ;
expr: INTEGER { $$ = $1; }  
         | expr '+' expr { $$ = $1 + $3; } 
         | expr '-' expr { $$ = $1 - $3; } 
;

%%
int yyerror(char *s) 
{ 
     fprintf(stderr, "%s\n", s); 
     return 0; 
}

其中,1表示右边的第一个标记的值,2表示右边的第二个标记的值,依次类推。$$表示归约后的值。

用户子例程段

yacc 将用户子例程段的内容完全拷贝到C文件中,通常这部分包括从动作调用的例程。
该部分是函数部分。当yacc解析出错时,会调用函数yyerror(),用户可自定义函数的实现。
main函数是调用yacc解析入口函数yyparse()。如:

int main(void) 
{ 
     yyparse(); 
     return 0; 
}

动作

动作 是yacc与在语法中规则相符时执行的C代码,动作一定是C复合语句。

动作有4种可能:

  • 移进:
    当Sj = GOTO[Si,a]成立,则把Sj移入到状态栈,把a移入到文法符号栈。其中i,j表示状态号。
  • 规约:
    当在栈顶形成句柄为β时,则用β归约为相应的非终结符A,即当文法中有 A-->β的产生式,而β的长度为r(即|β| = r),则从状态栈和文法符号栈中自栈顶向下去掉r个符号,即栈指针SP减去r。并把A移入文法符号栈内,再把满足Sj = GOTO[Si,A]的状态移进状态栈,其中Si为修改指针后的栈顶状态。
  • 接受acc:
    当规约到文法符号栈只剩文法的开始符号S时,并且输入符号串已结束即当前输入符是‘#’,则为分析成功。
  • 报错:
    当遇到状态栈顶为某一状态下出现不该遇到的文法符号时,则报错,说明输入串不是该文法能接受的句子。

通过使用后面跟有数字的美元符号,动作可以查阅在规则中与符号有关的值,冒号后面跟的第一个符号是数字1,例如:

date:month  '/'  day   '/'  year  
           { printf ("date %d-%d-%d  found",$1,$3,$5);}  

而名字,$$是指冒号左边符号的值,符号值可以有不同的C类型。

三、递归处理

递归处理有左递归和右递归。
左递归形式:

list: item 
    | list ',' item;

右递归形式:

list: item 
     | item ',' list

使用右递归时,所有的项都压入堆栈里,才开始规约;而使用左递归的话,同一时刻不会有超过三个项在堆栈里。

歧义和冲突

由于语法有歧义或者包含冲突,yacc对于语法规范的翻译可能会失败。一些情况下,语法确实有歧义,也就是说对于一个单独的输入字符串有两种可能的分析而且yacc处理不了。

另外一些情况,语法并无歧义,但yacc使用的语法分析技术不足以分析这个语法。

  • 移进/归约冲突
    当一个输入字符串有两种可能的分析时,而且其中一个分析完成一个规则(归约选项),而另一个却没有(移进选项)时,移进/归约冲突便发生了。
    例如:
%%  
e: ‘X’  
     |e  '+'   e  
         ;  

对于输入字符串“X+X+X” ,有两种可能的分析: “(X+X)+X”或者“X+(X+X)”,采用归约选项使得语法分析程序使用第一个分析,而采用移进选项则使用另一个。

  • 归约/归约冲突
    当同样的标记可以完成两个不同的规则时,就会发生归约/归约冲突。
    例如:
%%  
prog:    proga | progb  
proga:       'X' ;  
progb:       'Y' ;  

一个“X”可能是proga,也可能是progb。

大多数归约/归约冲突没这么明显,但是几乎在任何情况下它们在语法中都表现为错误。

  • If-Else 的冲突
    当有两个IF一个ELSE时,该ELSE和哪个IF匹配是一个问题。有两种匹配方法:与第一个匹配和与第二匹配。现代程序语言都让ELSE与最近的IF匹配,这也是yacc的缺省行为。
    虽然yacc行为正确,但为避免警告,可以给IF-ELSE语句比IF语句更高的优先级:
%nonassoc IFX 
%nonassoc ELSE
stmt: IF expr stmt %prec IFX 
       | IF expr stmt ELSE stmt

四、特殊字符

由于yacc处理符号标记而不是文本,它的输入字符集比起lex来说就简单的多,下面列出了yacc所使用的特殊符号的列表:

%
具有两个%标记的行将yacc语法分成了几部分;
定义段的所有声明都是以%开始,包括%{ %} %union %start %token %type %left %right 和 %nonassoc声明。

\
反斜线符号是废弃的百分号同义词,在动作中,C语言字符串中有其通常作用。

$
在动作中,美元符号引入一个值引用,举例来说,$3表示规则右端第3个符号的值。

'
文字标记由一个单引号结束,例如 'z' 。

<>
在一个动作的值引用中,可以不考虑尖括号包围起来的默认类型。

"
有些yacc版本在文字标记中将单引号和双引号同等对待,这样使用根本不方便。

{}
动作中C代码在大括号中。

;
除了后面紧接着是以竖线开头的规则外,规则部分每个都是以分号结束。

|
当连续两个规则具有相同的左端,第二个规则可用一个 | 代替符号和冒号。

:
在每一条规则里,左端的每个符号后面都跟着一个冒号。

_
符号可以包括和字母、数字以及句点在一起的下划线。

.
符号可以包括与字母、数字、下划线一起的句点。

=
早期版本使用,现已不推荐。

五、Yacc 源程序的风格

建议按照如下风格来写:

  1. 终端符名全部用大写字母,非终端符全部用小写字母;
  2. 把语法规则和语义动作放在不同的行;
  3. 把左部相同的规则写在一起,左部只写一次,而后面所有规则都写在竖线“|”之后;
  4. 把分号“;”放在规则最后,独占一行;
  5. 用制表符来对齐规则和动作。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • 编译原理 第一章 引言 1.从面向机器的语言到面向人类的语言 汇编指令:用符号表示的指令被称为汇编指令汇编语言:汇...
    SnorlaxSE阅读 53,976评论 5 60
  • 简介 如果你有Unix环境的编程经验,想必你肯定遇到过神秘的Lex和YACC工具,在GUN/Linux中,又分别称...
    upupSue阅读 4,338评论 0 7
  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,313评论 0 5
  • 他们一路无所顾虑的飞奔着,已经接近深夜,路上车辆行人都少了很多,若雪不顾方向的向前行进着,只要不会撞树,不会撞墙,...
    一心小记阅读 282评论 0 3
  • 文/赵元波 宋仁宗宝元元年,西夏的党项人围攻延安城七天,礼部尚书范雍任统帅领军防守。敌人不断进攻,延安城岌岌可危,...
    赵元波阅读 359评论 0 0