一.编译原理一些常识
1.词法分析将字符串按正则表达式,分成一个个匹配的token,予之表明它们身份的标记,以及长度和在语法文件中的位置。
2.单个的token没有意义,需要语法分析程序识别token的出现和它们排列的规则,按结构执行相应操作。
3.编译原理的研究
<1>.自然语言结构的研究。1957年NoamChomsky建立形式语言的描述后,人们将语言分类为0型-非限制的,1型-上下文有关的,2型-上下文无关,3型-正则的,后一种均为前一种的特例,且2型文法被证明是程序语言设计中最适用的。
<2>.正则表达式研究。正则表达式与3型文法直接对应并最终引出了token的符号方式。
<3>.自动机的研究。有穷自动机与下推自动机
<4>.计算机程序语言的发展又给予编译原理实验的场所。
4.语法分析的两种方案:自顶向下和自底向上分析法,后者有一种综合交通最好的分析技术LALR(1)分析法
二.语法分析生成器程序lemon
编译器的编译器,即生成语法分析器的编译器
1.语法分析生成器程序一次一次地输入一个个单词,寻求这些单词之间的关系
2.lemon的工作流程:
3.运行lemon(lemonxxx.y),生成一个能够进行语法分析的C源文件,一个定义终结符整数代号的头文件和一个包含语法分析所有状态的自动机信息报告文件.
4.lemon处理.y文件后,仅仅生成可供其他函数调用的一些子函数,这些子函数在语法分析器源码中。调用这些子函数,可以实现语法分析的功能
<1>.调用这些函数前,先要在应用程序创建一个分析器‘实体’:
void*pParser = ParseAlloc( malloc);
void*ParseAlloc(void *(*mallocProc)(size_t)){
yyParser*pParser;
pParser= (yyParser*)(*mallocProc)( (size_t)sizeof(yyParser) );
...
returnpParser;
}
ParseAlloc的参数是一个函数指针,该指针指向的函数用于给yyParser*pParser分配内存,我们可用C标准函数malloc(),也可用自己编写的分配内存的函数。
完成语法分析后,要记得删除pParser:ParseFree(pParser,free);
free就是C标准函数free(),也可用程序员自己的内存释放函数。
<2>.运行分析器前,先要向它提供三个输入:
记号的类型,如记号‘123’的类型是INT;
记号的值,如‘123’这个字串
第三个输入实际上是转手给.y文件中产生式右边的C代码的,这个输入不是必要的
voidParse(
void*, /* 分析器void*pParser */
int , /* 记号的类型 */
ParseTOKENTYPE, /* 记号的值,这个类型是程序员自定义的,是.y文件中占位符的类型;占位符是规则段小括号中的大写字母,表示记号的值,又因记号一般不会只有一个类型,因此ParseTOKENTYPE可能是一个结构体,结构中包含void*指针指向真正的记号的值,并用一个整数标记指明该记号的类型*/
ParseARG_PDECL/* 可选的额外参数,类型按需求指定*/
)
Parse()调用语法分析器进行分析;第一个参数是分析器,后三个分别对就前面的三个输入
<3>.一个例子
ParseTree*ParseFile(const char *fileName){
Tokenizer*pTokenizer; /* 符号流容器;程序员自定义的类型,一般是一个结构体,多个这样的结构体组成链表用来存放所有的记号*/
void*pParser;
TokenaToken; //程序员自定义类型,
inthTokenId;
ParserState sState; /*自定义类型,可用来保存生成的语法树,也可用来向递归式右边的C代码传递信息*/
pTokenizer= TokenizerCreate(fileName); /*一个自定义函数, 从文件fileName中取出字符串,分解成一个个记号类 hTokenId和记号值aToken,存入符号流容器中,这些功能都需要程序员自己实现,一般是借助词法分析工具lex*/
pParser= ParseAlloc(malloc);
InitParserState(sState); //程序员自定义
/*GetNextToken(),自定义函数,输入符号流容器,每调用一次,通过后两个参数输出一个记号类型和记号值*/
while(GetNextToken(pTokenizer, &hTokenId, &aToken) ) {
Parse(pParser,hTokenId, aToken, &sState);
}
Parse(pParser,0, aToken, &sState);
ParseFree(pParser,free);
TokenizerFree(pTokenizer);
return sState.treeRoot; /*返回语法树要节点*/
}
4.非终结符类似于变量,终结符类似变量的值
5.lemon语法文件中的产生式(递归式)
左边是一个非终结符,随后是表达产生的符号”::=”,右边是一系列终结符和非终结符,产生式以'.'结尾,最右边是栓测到符合产生式结构时所执行的C代码
如:expr:: expr + expr. {printf(“a plus expression”);}
上例表明了产生式和随后C代码的关系,但不能操作产生式中的各个符号。
我们使用占位符来代表符号的值:
expr(A)::= expr(B) + expr(C). {printf(“%d = %d + %d \n”, A, B, C);}
A,B,C分别表达第一,二,三个符号的值,如果一个产生式是x= 5 +7,那么A就是12,B是5,C是7。
6.冲突和优先级
一个方法可能有多种含义,在程序语言不允许有二义性。我们用优先级和结合律消除二义性。
左结合:优先级相同时,从左往右运算
右结合:...
%leftOR. //OR优先级最低,因为排在最前,%left又说明它是左结合的
%leftAND. //AND 比 OR优先级高一级,同样也是左结合
%nonassocEQ NE GT. // EQ NE GT不能在同一产生式中出现两次或两次以上
7.几个特殊申明符
<1>.%token_type,%type
所有终结符和非终结符,它们都有像int或其他形式的类型。
但像voidParse(void *, int , ParseTOKENTYPE,ParseARG_PDECL)类似在函数中,第三个参数的类型是固定的。
假设一个文法的终结符有int或string两种类型,那就必须有一个void*指针来灵活指向,并用一个标记来区分两种类型:structtokenType {int type, void *pValue}。(也可以用一个联合体uniontokenType {int number, char *string})
然后我们就可以用%token_type将所有终结符类型都定义为tokenType*:
%token_type{tokenType*}
如果没有用上面的方式指定终结符的统一类型时,lemon会自动指定为void
<2>.用%default_type定义所有非终结符的统一类型
<3>.用%include指定C代码,如包含头文件,声明函数原型等,放于语法文件的头部。
<4>.%left,%rignt,%nonassoc
<5>.%extra_argument指定语法分析时输入的第四个参数。
<6>.%token_prefix。lemon会在生成的语法分析器源码头文件中用#define定义一些小整数来标识终结符类型,例:
#definesAND 1
#definesOR 2
#definesPLUS 3
用%token_prefix指定前缀:
%token_prefixTOK_
最终在语法分析器源码头文件中,前面的三个类型标识定义就变成:
#definesTOK_AND 1
#definesTOK_OR 2
#definesTOK_PLUS 3
<7>.%stack_size,修改语法分析栈大小
<8>.%stack_overflow,指定分析栈溢出时的行为。
如%stack_overflow{printf(“syntax stack overflow! \n”);}
<9>.syntax_error,指定出现语法错误时的行为。