Bison Quick Tutorial

转自:点击打开链接

Contents

 [hide

GNU Bison 简介

  Bison的前身为基于Unix的Yacc。令人惊讶的是,Yacc的发布时间甚至比Lex还要早。Yacc所采用的LR分析技术的理论基础早在50年代就已经由Knuth逐步建立了起来,而Yacc本身则是贝尔实验室的S.C.Johnson基于这些理论在75年到78年写成的。到了1985年当时在UC Berkeley的一个研究生Bob Corbett在BSD下重写了Yacc,后来GNU project接管了这个项目,为其增加了许多新的特性,于是就有了我们今天所用的GNU Bison。   GNU Bison在Linux下的安装非常简单。你可以去它的官方网站上下载安装包自行安装,基于Debian的Linux系统下更简单的方法同样是直接在命令行敲入如下命令:

 > sudo apt-get install bison

  虽说版本不一样,但GNU Bison的基本使用方法和教材上所介绍的Yacc没有什么不同。首先,我们需要自行完成包括语法规则等在内的Bison代码。如何编写这份代码后面会提到,现在先假设这份写好的代码名syntax.y;随后,我们使用Bison对这份代码进行编译:

 > bison syntax.y

  编译好的结果会保存在当前目录下的syntax.yy.c文件中。打开这个文件你会发现,该文件本质上就是一个C语言的源代码。事实上,这份源代码里目前对我们有用的函数只有一个,叫做yyparse(),该函数的作用就是对输入文件进行语法分析,如果分析成功没有错误则返回0,否则返回非0。不过,只有这个yyparse()函数还不足以让我们的程序跑起来——前面说过,语法分析器的输入是一个个的词法单元,那么Bison通过什么方式来获得这些词法单元呢?事实上,Bison在这里需要用户为它提供另外一个专门返回词法单元的函数,这个函数名叫yylex()。

Flex与Bison联合编译

  yylex()相当于嵌在Bison里的词法分析器。这个函数可以由用户自行实现,但是因为我们之前已经使用Flex生成了一个yylex()函数,能不能让Bison使用Flex生成过的yylex()函数呢?答案是肯定的。   仍然以Bison源代码syntax.y为例。首先,为了能够使用Flex中的各种函数,需要在Bison源代码中引用lex.yy.c:

  1. #include "lex.yy.c"

  随后在使用Bison编译这份代码时,我们需要加上“-d”参数:

 > bison -d syntax.y

这个参数的含义是,将编译的结果分拆成syntax.tab.c和syntax.tab.h两个文件,其中.h文件里包含着一些词法单元的类型定义之类的内容。得到这个.h文件以后,下一步是修改我们的Flex源代码lexical.l,增加对syntax.tab.h的引用,并且让Flex源码中规则部分的每一条action都返回相应的词法单元,如下图所示:

  1. %{
  2. 	#include “syntax.tab.h”
  3. %}
  4. %%
  5. +{ return PLUS; }
  6. -{ return SUB; }
  7. &&{ return AND; }
  8. ||{ return OR; }

其中,返回值PLUS、SUB等都是在Bison源代码中定义过的词法单元(如何定义它们后文会提到)。由于我们刚刚修改了lexical.l,需要重新将它编译出来:

 > flex lexical.l

  接下来是重写我们的main函数。由于Bison会在需要时自动调用yylex(),我们在main函数中也就不需要调用它了。不过,Bison是不会自己调用yyparse()和yyrestart()的,因此这两个函数仍需要我们在main函数中显式地进行调用:

  1. int main(int argc, char** argv)
  2. {
  3. 	if (argc <= 1) return 1;
  4. 	FILE* f = fopen(argv[1], "r");
  5. 	if (!f)
  6. 	{
  7. 		perror(argv[1]);
  8. 		return 1;
  9. 	}
  10. 	yyrestart(f);
  11. 	yyparse();
  12. 	return 0;
  13. }

  现在我们有了3个C语言源文件:main.c、lex.yy.c以及syntax.tab.c,其中lex.yy.c已经被syntac.tab.c引用了,因此我们最后要做的就是把main.c和syntax.tab.c放到一起进行编译:

 > gcc main.c syntax.tab.c -lfl -ly -o parser

其中“-lfl”不要省略,否则GCC会因缺少库函数而报错,但“-ly”这里一般情况下可以省略。现在我们就可以使用这个parser程序进行词法分析了。例如,想要对一个测试文件test.cmm进行语法分析,只需要在命令行输入:

 > ./parser test.cmm

就可以得到你想要的结果。

编写Bison源代码

  上面介绍的是使用Flex和Bison联合创建语法分析器的基本步骤。在整个创建过程中,最重要的文件无疑你所编写的Flex源代码和Bison源代码,它完全决定了你所生成的语法分析器的一切行为。Flex源代码如何进行编写前面已经介绍过了,接下来这段教程将指导你如何去编写Bison源代码。

  同Flex源码一样,Bison源代码也分为三个部分,其作用与Flex代码大致相同:第一部分是声明部分,所有词法单元的定义都可以放到这里;第二部分是规则部分,其中包括具体的语法和相应的语义动作;第三部分是用户函数部分,这一部分代码会原封不动地拷贝到syntax.tab.c中,方便用户自定义需要执行的函数(main函数也可以写在这里,不过同样不推荐这么做)。值得一提的是,如果用户想要对这一部分所用到的变量、函数或者头文件进行声明,可以在定义部分(也就是Bison源代码的第一部分)之前使用“%{”和“%}”符号将要声明的内容添加进去。被“%{”和“%}”所包围的内容也会一并拷贝到syntax.tab.c的最前面。

  下面我们通过一个例子来对Bison源码的结构进行解释。一个在控制台运行的可以进行整数四则运算的小程序,其语法如下图所示(这里假设词法单元INT代表Flex识别出来的一个整数, ADD代表加号+,SUB代表减号-,MUL代表乘号*,DIV代表除号/):

  1. Calc	→	ε
  2. 	|	Exp
  3. Exp	→	Factor
  4. 	|	Exp ADD Factor
  5. 	| 	Exp SUB Factor
  6. Factor	→	Term
  7. 	|	Factor MUL Term
  8. 	|	Factor DIV Term
  9. Term	→	INT

  这个程序完整的Bison代码为:

  1. %{
  2. 	#include <stdio.h>
  3. %}
  4.  
  5. /* declared tokens */
  6. %token INT
  7. %token ADD SUB MUL DIV
  8.  
  9. %%
  10. Calc	:	/* empty */
  11. 	|	Exp	{ printf(= %d\n”, $1); }
  12. 	;
  13. Exp	:	Factor
  14. 	|	Exp ADD Factor		{ $$ = $1 + $3; }
  15. 	|	Exp SUB Factor	{ $$ = $1 - $3; }
  16. 	;
  17. Factor	:	Term
  18. 	|	Factor MUL Term	{ $$ = $1 * $3; }
  19. 	|	Factor DIV Term	{ $$ = $1 / $3; }
  20. 	;
  21. Term	:	INT
  22. 	;
  23. %%
  24. #include “lex.yy.c”
  25. int main() {
  26. 	yyparse();
  27. }
  28.  
  29. yyerror(char* msg) {
  30. 	fprintf(stderr, “error: %s\n”, msg);
  31. }

  这段Bison代码以%{…%}开头,被%{…%}包含的内容主要是对stdio.h的引用。接下来是一些以%token开头的词法单元(终结符)定义,如果你需要采用Flex生成的yylex()的话,那么在这里定义的词法单元都可以作为Flex源代码里的返回值。与终结符相对地,所有未被定义为%token的符号都会被看作非终结符,这些非终结符要求必须在任意产生式的左边至少出现一次。

  接下来的第二部分就是书写产生式的地方。第一个产生式左边的非终结符默认为初始符号(你也可以通过在定义部分添加%start X来将另外的某个非终结符X指定为初始符号)。产生式中的箭头在这里用冒号(:)表示,一组产生式和另一组之间以分号(;)隔开。产生式中无论是终结符还是非终结符都各自对应一个属性值,产生式左边的非终结符对应的属性值用$$表示,右边的几个符号的属性值按从左到右的顺序依次表示为$1、$2、$3……每一条产生式的最后可以添加一组以花括号{}括起来的语义动作,这组语义动作会在整条产生式归约完成之后执行,如果不明确指定语义动作,那么Bison将采用默认的语义动作{ $$ = $1 }。语义动作也可以放在产生式的中间,例如A→B {…} C,这样的写法等价于A→BMC,M→ε{…},其中M为额外引入的一个非终结符。需要注意的是,在产生式中间添加语义动作在某些情况下有可能会在原有语法中引入冲突,因此使用的时候要特别谨慎。

  看到这里,不知道你的心里有没有产生一个疑问:每一个非终结符的属性值都可以通过它所产生的那些终结符或者非终结符的属性值计算出来,但是终结符本身的属性值如何得到呢?答案是在yylex()函数中得到。因为我们的yylex()函数是由Flex源代码生成的,因此要想让终结符带有属性值,就必须回头修改Flex源代码。假设在我们的Flex源代码中,INT词法单元对应着一个数字串,那么我们可以将Flex源码修改为:

  1. digit	[0-9]
  2. %%
  3. {digit}*	{
  4. 		yylval = atoi(yytext);
  5. 		return INT;
  6. 	}
  7. %%

变量yylval是Flex的内部变量,意为当前词法单元所对应的属性值。我们只需要将这个变量赋成atoi(yytext)就可以将词法单元INT的属性值设置为它所对应的整数值了。

  回到之前的Bison代码中。代码的用户自定义函数部分我们写了两个函数:一个很简单的只调用了yyparse()的main函数以及另一个没有返回类型并带有一个字符串参数的yyerror()的函数。yyerror()函数是Bison提供的库函数,它会在你的语法分析器每发现一个语法错误时被调用,默认参数为”syntax error”。默认情况下yyerror()只会将传入的字符串参数打印到标准错误输出上,而你可以自己重新定义这个函数从而使它打印一些更加用户友好的内容,例如出错的行号、出错那行的源代码等。上例中我们就在参数前面多打印了”error: “字样。如果你对什么是“用户友好”的错误信息不是很清楚的话,可以尝试安装并使用一个叫clang的编译器(Debian用户直接sudo apt-get install clang即可),看看一个实用的编译器输出的错误信息可以详细到什么程度。另外,如果 你想要知道一个实用的编译器输出的错误信息可以好玩到什么程度,请参考这个链接

  现在,编译并执行这个程序,然后在控制台输入10-2+3,然后输入回车,最后输入Ctrl+D结束,你会看到屏幕上打印出了计算结果11。

属性值的类型

  不知道你发现了没有,在上面的例子中,每一个终结符以及非终结符的属性值都是int类型。但在我们构建语法树的过程中,我们非常希望不同的符号对应的属性值能有不同的类型,而且最好能对应任意类型而不仅仅是int型。这一节内容将会指导你如何在Bison中解决上述问题。

  第一种方法是对宏YYSTYPE进行重定义。Bison里会默认所有属性值的类型以及变量yylval的类型都是YYSTYPE,默认情况下YYSTYPE被定义为int。如果你在你的Bison代码的%{…%}部分加入例如这样一句话#define YYSTYPE float,那么所有属性值就都成为float型了。那么如何使得不同的符号对应不同的类型呢?你可以将YYSTYPE定义成一个联合体(union)类型,这样你就可以根据符号的不同来访问联合体中不同的域,从而实现多种类型的效果。

  上面这种方法虽然可行,但在实际操作中还是稍显麻烦,因为你每次对属性值的访问都要自行指定哪个符号对应哪一个域。实际上,在Bison中已经内置了其他的机制来方便你对属性值类型的处理,一般而言我还是更推荐使用这种方法而不是上面介绍的那种。

  仍然还是以前面四则运算的小程序为例说明Bison中的属性值类型机制是如何工作的。原先这个四则运算程序只能计算整数值,现在我们加入浮点数运算的功能。修改后的语法如下图所示:

  1. Calc	→	ε
  2. 	|	Exp
  3. Exp	→	Factor
  4. 	|	Exp ADD Factor
  5. 	| 	Exp SUB Factor
  6. Factor	→	Term
  7. 	|	Factor MUL Term
  8. 	|	Factor DIV Term
  9. Term	→	INT
  10. 	|	FLOAT

  在这一份语法中,我们希望词法单元INT能有整型属性值,FLOAT能有float型属性值,其他的非终结符为了简单起见我们让它们都具有double型的属性值。这份语法以及类型方案对应的Bison源代码如下:

  1. %{
  2. 	#include <stdio.h>
  3. %}
  4.  
  5. /* declared types */
  6. %union {
  7. 	int type_int;
  8. 	float type_float;
  9. 	double type_double;
  10. }
  11.  
  12. /* declared tokens */
  13. %token <type_int> INT
  14. %token <type_float> FLOAT
  15. %token ADD SUB MUL DIV
  16.  
  17. /* declared non-terminals */
  18. %type <type_double> Exp Factor Term
  19.  
  20. %%
  21. Calc	:	/* empty */
  22. 	|	Exp	{ printf(= %lf\n, $1); }
  23. 	;
  24. Exp	:	Factor
  25. 	|	Exp ADD Factor	{ $$ = $1 + $3; }
  26. 	|	Exp SUB Factor	{ $$ = $1 - $3; }
  27. 	;
  28. Factor	:	Term
  29. 	|	Factor MUL Term	{ $$ = $1 * $3; }
  30. 	|	Factor DIV Term	{ $$ = $1 / $3; }
  31. 	;
  32. Term	:	INT	{ $$ = $1; }
  33. 	|	FLOAT	{ $$ = $1; }
  34. 	;
  35. %%

  首先,我们在定义部分的开头使用%union{…}将所有可能的类型都包含进去。接下来,在%token部分里我们使用一对尖括号<>把需要确定属性值类型的每个词法单元所对应的类型括起来。对于那些需要指定其属性值类型的非终结符而言,我们使用%type加上尖括号的办法确定它们的类型。当所有需要确定类型的符号的类型都被定下来之后,规则部分里的$$、$1等就自动地带有了相应的类型,不再需要我们显示地为其指定类型了。

语法单元的位置

  实习要求中需要你输出每一个语法单元出现的位置。你当然可以自己在Flex中定义每个行号和列号、在每一个语义动作中维护这个行号和这个列号并将它们作为属性值的一部分返回给语法单元。这种做法需要我们额外编写一些维护性的代码,让人感觉挺不方便。Bison有没有内置的位置信息供我们使用呢?答案是肯定的。

  前面介绍过Bison中的每一个语法单元都对应了一个属性值,在语义动作中这些属性值可以使用$$、$1、$2等进行引用。实际上除了属性值以外,每一个语法单元还对应了一个位置信息,在语义动作中这些位置信息同样可以使用@$、@1、@2等进行引用。位置信息的数据类型是一个YYLTYPE,其默认的定义是:

  1. typedef struct YYLTYPE
  2. {
  3. 	int first_line;
  4. 	int first_column;
  5. 	int last_line;
  6. 	int last_column;
  7. }

其中first_line和first_column分别是该语法单元对应的第一个词素出现的行号和列号,而last_line和last_column分别是该语法单元对应的最后一个词素出现的行号和列号。有了这些内容,输出位置信息时我们就显得游刃有余了。看到这里我假设你会高高兴兴地回去修改代码,引用@1、@2等将每一个语法单元的first_line打印出来,结果发现打印出来的行号全都是1……

  为什么会出现这种问题?主要原因在于,Bison并不会替我们维护这些位置信息,我们必须在Flex源文件中自行维护。看到这里你又会说,如果要我们自己去维护,那不就相当于使用本节开头介绍的那个笨方法了吗?其实,只要稍加利用Flex中的某些机制,维护这些信息并不需要太多的代码。我们在Flex源文件开头部分定义变量yycolumn并添加这样宏YY_USER_ACTION:

  1. %{
  2. /* 此处省略#include部分 */
  3. 	int yycolumn = 1;
  4.  
  5. 	#define YY_USER_ACTION yylloc.first_line = yylloc.last_line = yylineno; \
  6. 		yylloc.first_column = yycolumn; yylloc.last_column = yycolumn + yyleng - 1; \
  7. 		yycolumn += yyleng;
  8. %}

其中yylloc是Flex内置变量,代表当前词法单元所对应的位置信息;YY_USER_ACTION宏代表在执行每一个语义动作之前需要先被执行的一段代码,默认为空,而这里我们将其改成了对位置信息的维护代码。最后还要在Flex源文件中做的更改就是在发现换行符以后对变量yycolumn进行复位:

  1. ...
  2. %%
  3. ...
  4. \n	{ yycolumn = 1; }

  好了,现在回到Bison中再去打印位置信息,是不是已经正常了?

思考题
如果仔细考察上面对YY_USER_ACTION的定义,你会发现yylloc.first_line和yyloc.last_line总是被赋成一样的值。但当我们在Bison中打印出每一个语法单元的first_line和last_line时很多语法单元的这两个值并不相同。这是怎么回事呢?

二义性与冲突处理

  Bison的一个非常好用同时也是一个非常恼人的特性,是即使对于一个有二义性的文法,它也会有自己的一套隐式的冲突解决方案(我们知道,一旦出现归约/归约冲突,Bison总会选择靠前的产生式;而一旦出现移入/规约冲突,则Bison总会选择移入)从而生成相应的语法分析器,而这些冲突解决方案在某些场合有可能并不是我们所期望的。因此,我建议大家在使用Bison编译代码时要留意它所给的提示信息,如果提示文法有冲突,那么请一定对源码进行修改,尽量把所有的冲突全部消解掉。

  前面我们的那个四则运算的小程序,如果它的语法变成这样:

  1. Calc	→	ε
  2. 	|	Exp
  3. Exp	→	Factor
  4. 	|	Exp ADD Exp
  5. 	| 	Exp SUB Exp
  6. ...

  虽然看起来好像没什么变化(Exp→Exp ADD/SUB Factor现在变成了Exp→Exp ADD/SUB Exp),但实际上前文里之所以没有这样写是因为这样做会引入额外的二义性。例如,输入为1-2+3,程序到底是先算1-2呢,还是2+3呢?恐怕是先算后者。此时语法分析器在读到1-2的时候可以归约也可以移入,但由于Bison默认移入优先于归约,因此语法分析器会继续读入+3然后计算2+3。为了解决这里出现的二义性问题,要么重写语法(Exp→Exp ADD/SUB Factor相当于强制规定加减法为左结合),要么显示地指定算符的优先级与结合性。一般而言,重写语法总是一件比较麻烦的事情,而且会引入不少像Exp→Term这样除了增加可读性之外没什么实质用途的产生式。所以更好的解决办法还是考虑优先级与结合性。

  在Bison源代码中,我们可以通过%left、%right和%nonassoc对终结符的结合性进行规定,其中%left表示左结合,%right表示右结合,%nonassoc表示不可结合。例如,下面这段结合性的声明代码主要针对四则运算、括号以及赋值号:

  1. %right	ASSIGN
  2. %left	ADD	SUB
  3. %left	MUL	DIV
  4. %left	LP	RP

其中ASSIGN代表赋值号,LP代表左括号,RP代表右括号。实际上,Bison规定任何排在后面的算符其优先级都要高于排在前面的算符。因此,这段代码实际上还在规定括号优先级高于乘除、乘除高于加减、加减高于赋值号。在我们实习所使用的C--语言里,表达式Exp的语法便是高度冲突的,你需要模仿前面介绍的方法,根据C--语法补充说明中的内容为运算符规定优先级和结合性,从而解决掉这些冲突。

  另外一个在程序设计语言中非常常见的冲突是嵌套if-else所出现的冲突(也被称为“悬空else”)。考虑C--语言的这段语法:

  1. Stmt	→	IF LP Exp RP Stmt
  2. 	|	IF LP Exp RP Stmt ELSE Stmt

  假设我们的输入是if (x > 0) if (x == 0) y = 0; else y = 1; ,那么语句最后的这个else是属于前一个if还是后一个if呢?标准C语言规定在这种情况下else总是匹配距离它最近的那个if,这与Bison的默认处理方式(移入/归约冲突时总是移入)是一样的,因此即使我们不在Bison源代码里对这个问题进行任何处理,最后生成的语法分析器的行为也是正确的。但如果不处理,Bison总是会提示我们的语法中存在一个移入/规约冲突,让人很不爽。有没有办法把这个冲突去掉呢?

  显式地解决悬空else问题可以借助于算符优先级。Bison源代码中每一条产生式后面都可以紧跟一个%prec标记,指明该产生式的优先级等同于一个终结符。下面这段代码通过定义一个比ELSE优先级更低的LOWER_THAN_ELSE算符,降低了规约相对于移入ELSE的优先级:

  1. %nonassoc	LOWER_THAN_ELSE
  2. %nonassoc	ELSE
  3. %%
  4. Stmt	:	IF LP Exp RP Stmt	%prec LOWER_THAN_ELSE
  5. 	|	IF LP Exp RP ELSE Stmt

这里ELSE和LOWER_THAN_ELSE的结合性其实并不重要,重要的是当语法分析器读到IF LP Exp RP时,如果它面临归约和移入ELSE这两种选择,它会根据优先级自动选择移入ELSE。通过指定优先级的办法,我们可以避免Bison在这里报告冲突。

思考题
如果我们要做一个奇怪的程序设计语言,每一个else都匹配距离它最远的那个if,应该如何修改Bison代码?仅仅把ELSE和LOWER_THAN_ELSE的优先级对调一下就行了吗?

  前面我们通过优先级和结合性解决了表达式和if-else语句里可能出现的二义性问题。事实上,有了优先级和结合性的帮助,我们几乎可以消除语法中所有的二义性——但我强烈不建议大家使用它们解决除了表达式和if-else之外的任何冲突。原因很简单:只要是Bison报告的冲突,都有可能成为语法中潜在的一个缺陷,这个缺陷的来源很可能是你所定义的程序设计语言里的一些连你自己都没有意识到的语法问题。表达式和二义性这里我们之所以敢使用优先级和结合性是因为我们对冲突的来源非常了解,除此之外,只要是Bison认为有二义性的语法,大部分情况下这个语法让人来看肯定也会看出二义性。此时你要做的不是试图掩盖这些语法上的问题,而是应该坐下来仔细地对语法进行修订,发现并解决语法本身的问题。

思考题
我们知道,在C++语言中,当我们声明了一个类class MyClass {…}之后,在使用这个类定义对象x时可以直接写成MyClass x;。而如果你细心观察C语言或者C--语言的语法,会发现当我们声明了一个结构体,例如struct Complex {…}之后,在使用这个结构体定义变量x时必须写成struct Complex x; 而不能直接写成Complex x; ,感觉有些不方便。现在我们想尝试修改一下C--的语法使得后一种写法合法化:把Specifier的产生式修改为Specifier → TYPE | ID | StructSpecifier。试问这样的修改可行吗?如果不可行,会出现什么样的问题?怎样做才能不出现问题?

错误恢复

  当输入文件中出现语法错误的时候,Bison总是会让它生成的语法分析器尽早地报告错误。每当语法分析器从yylex()得到了一个词法单元,但这个当前状态并没有针对这个词法单元的动作时,就认为输入文件出现了语法错误,此时它会默认进入下面这个错误恢复模式:

  • 调用yyerror(“syntax error”),只要你没有重写yyerror(),该函数默认会在屏幕上打印出syntax error字样
  • 从栈顶弹出所有还没有处理完的规则,直到语法分析器回到了一个可以移入特殊符号error的状态
  • 移入error,然后对输入的词法单元进行丢弃,直到找到一个能够跟在error之后的符号为止(该步骤也被称为resynchronization)
  • 如果在error后能成功移入三个符号,则继续正常的语法分析;否则,返回前面的步骤2

需要注意的是,error本身也是一个语法单元。我们在书写产生式A→B error C时也就意味着,当栈顶出现了B、error、C三个语法单元时,语法分析器会尝试进行归约,将这三个语法单元弹栈并压入新语法单元A。除了是由语法分析器自行压入而不是来源于输入文件这一点以外,error跟其它的语法单元并没有什么不同。

  前面这个步骤看起来似乎很复杂,但实际上要我们做的事情只有一件:在语法里指定一下error符号放到哪里即可。不过,这个看似简单的工作其实蛮有讲究的:一方面,我们希望error后面跟的内容越多越好,这样resynchronization就会更容易成功,这提示我们应该把error尽量放在高层的产生式中;另一方面,我们又希望能够丢弃尽可能少的词法单元,这提示我们应该把error尽量放在底层的产生式中。在实际应用中,人们一般把error放在例如行尾、括号结尾等地方,本质上相当于让行结束符“;”以及括号“{}”“()”等作为错误恢复的同步符号。例如:

  1. Stmt	→	error SEMI
  2. CompSt	→	error RC
  3. Exp	→	error RP

  当然,上面这几个产生式仅仅是几个示例,并不意味着把这几个产生式照搬到你的Bison源代码中就可以让语法分析器能够满足本次实习的实习要求。你需要自己思考如何书写包含error的产生式才能够检查出输入文件中存在的各种语法错误——注意这并不是一项简单工作,目前为止已经有多人向我询问如何进行error产生式书写了。 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值