写在前面
其实这个是我的编译原理一次作业的草稿,也是第一次阅读编译器的源码,虽然比较简单,但是也是第一次,分享一下吧。
分析set
在set.c中,包含了自定义的set.h文件。
set.h
set.h中定义了一个结构体,组成单链,并声明了一些函数。这里需要注意的是,程序包含了stdarg.h,使得函数可以使用可变参数。
set.c
setinsert
功能比较简单,在链表中寻找第一个比传入数字大的数字的单元,并在其后插入一个单元,这个单元中包含函数形参中的数字。
createset
建立链表,并将参数中的可变参数一一构成结构体,插入链表,然后返回该链表。
uniteset
将两个链表按照从小到大的顺序合成为一个链表。
destroyset
销毁传入链表。
inset
判断传入的数字是否在链表内
showset
打印链表中所有的数字
分析pl0
在pl0.c中,包含了set.h和pl0.h。
pl0.h
首先程序定义了四个枚举型变量,其中symtype放置了各种词法的记号名,idtype放置了id的类型,。。。。。。定义了
pl0.c
首先分析main函数。
main函数首先打开了两个文件,一个作为输入文件,一个作为输出文件。之后建立null链表,判断符链表,逻辑控制符链表,。。。链表。并定义了一些变量,用来表示字符个数,长度,是否出错和当前指令索引等。设置当前的所读字符为' ',完成一系列的准备工作后,main函数调用了getsym函数、block函数和interpret函数,先进行分析
getsym
error
这个函数主要用来报错,参数是报错信息的index。
将之前识别的字符全部清为空格,然后err变量加一。
getch
这个函数主要用来漏掉空格,读取一个字符
如果识别完一行,将行长和字符计数清零,将当前指令索引以十进制写入输出文件,并输出至DOS。之后将下一行加入line数组,并将其写入输出文件。如果未识别完一行,则将当前行数组的下一个字符赋值给当前正在读的字符。
getsym
函数功能是词法分析出一个词法单元。
函数开始时规定,如果当前读入的字符为空格或者制表符,则调用getch函数,得到当前字符,之后判断字符类型,如下:
字母
则将字母和数字全部放入a数组,之后将这个一串字符串与保留字比较。
如果是保留字,则将当前识别的符号标示为该保留字。
如果不是保留字,则将其识别为标识符。
数字
将所有的数字接收入字符串,并将字符串转换为数字,并放入num变量中。如果字符串长度大于14,则报错,表明该数字太大。
冒号
再接收一个字符,如果下一个字符不为等号,则报错。
如果是等号,则识别为赋值符号。并接收下一个符号。
大于号和小于号
判断其判断符的类型,并将其归类,方法与上述类似,不做赘述。并接收下一个字符。
其他
主要是判断是否是运算符。过程和上面类似。
之后,程序建立了一个含有小数点的链表,并将之前的判断符链表和逻辑控制符链表合并为一个链表,之后以这个新链表为参数调用block函数。下面分析block函数。
block
分析block之前也是一样,分析调用在其之前的函数:
gen
这个函数是将指令索引加入code数组。
首先判断指令索引是否大于最大值(500),如果大于则报错,程序太大,否则,加入数组
test
这个函数主要功能是检测错误并恢复。
这个函数有三个参数,两个链表,一个错误编号。判断当前符号的类型是否在第一个链表,如果在则什么都不做,否则,打印s1和s2的内容,然后调用error 函数,报错一行,然后将两个链表合并,之后判断当前符号的类型是否在这个合并的链表中,如果在直接释放链表,如果不在,则一直向后分析,直到找到符合的字符。
这个就是最简单的错误恢复策略"紧急方式"的恢复。
position
该函数的作用是寻找标识符在符号表的位置。
enter
这个函数的主要功能是判断标识符的类型。
先判断标识符是否已被定义,如果有则报错,否则将其加入符号表。之后判断标识符的类型,分为以下几类:
常量
直接将数值加入符号表
变量
则将其放入另一个表,将其层次和地址放入mask表。
保留字
将其放入mask表,与变量类似,但不放入地址。
constdeclaration
这个函数的主要功能是识别常量。
如果当前词法单元不是标识符则报错,否则判断其后其后是否有等号或者赋值号。若不是报错,再接收一个词法单元,判断类型是否为数字,若是则将其以常量递归调用自身,写入符号表。
vardeclaration
这个函数的主要功能是识别变量。
如果当前词法单元是标识符直接以变量放入符号表,否则报错。
listcode
这个函数用于打印代码的信息,并将其写入输出文件。
term
这个函数的主要功能是提供乘除语法分析。
将乘除链表和传入链表合并,判断当前符号是否为乘或除,之后分别进行语法分析和执行翻译方案。
expression
这个函数的主要功能是提供表达式语法分析。
将加减链表和传入链表合并,判断当前符号是否为加或减,或者调用乘除的运算的语法分析。之后分别进行语法分析和执行翻译方案。
factor
这个函数的主要功能是提供的因子语法分析。
首先判断是否出错,也就是判断当前符号是否是标识符、数字或左括号。
然后判断当前字符类型:
标识符
判断该标识符是否被定义,若未定义则报错。之后判断该标识符类型。
若为常量,将其level设置为0,还有其数值送入code数组。
若为变量,将其level还有其地址送入code数组。
若为保留字,则报错。
数字
判断其大小是否合法,之后将其和常量一个处理方法写入code表。
左括号
继续接收一个词法单元,将右括号链表和传入的链表合并。之后判断其是否符合expression的语法规则,最后必须以右括号结尾。
condition
这个函数的主要功能是提供条件语法分析。
首先判断当前词法单元类型是否为odd,若符合,则判断当前是否符合表达式语法规则。
若不为odd,则判断条件的类型,并分类加入code数组
statement
这个函数的主要功能是提供语句部分分析。
识别当前词法符号类型:
标识符
首先判断其是否定义,之后判断其是否其类型是否为变量,若不为报错。然后接收下一个词法单元,判断其是否为赋值号,若不为报错,若为,则继续接收下一个词法单元,之后判断之后是否为表达式,若是,则将其写入code。
call
接收下一词法单元,判断其是否为标识符,进而判断其是否为程序名,若是将其写入code,否则报错。
if
接收下一词法单元,然后判断其是否符合条件语法,之后将其写入code表,之后判断其后是否为statement,之后将其的的索引也写入之前的code表中的偏移地址。
begin
接收下一词法单元,然后判断其是否符合statement,然后判断其是否为;或者statement符号,若条件不为假则反复调用statement。最后以判读以end结尾。
while
接收下一词法单元,然后判断其是否符号条件的语法,之后,将其加入code,再之后接收do,在do后必须跟这statment。
block
这个函数的主要功能就是分析程序处理过程,也是开始符。
将当前的指令索引的地址置0,之后将跳转的指令加入code作为开始的指令。
之后如果判断到了声明的类型的词法单元,则接收一个词法单元判断其类型。
若为常量标识const,则反复接收id,其中用逗号隔开,以分号结尾。
若为变量标识var,则反复接收id,其中用逗号隔开,以分号结尾。
之后识别过程标识procedure,接收一个id将其放入符号表,或者接收分号,调用自身,若之后字符为分号,检查之后的错误。
最后检查错误,判断词法单元是否为语句开始。
在此之后,函数开始做结尾处理,识别end或者分号。之后将其列出。
之后分析interpret函数。
interpret
base
这个函数单看不太理解,结合后面的使用,stack[top + 1] = base(stack, b, i.l);
,这里的b是类似ebp的话,stack[b]就是放置着栈基址的值,也就是最后返回的是变量值。结合后面来看,特别是后面的cal部分,pl0的栈应该是以分层区别不同函数的栈,这个也就是这个level指的是不同的层次。那么这个函数就是用来计算相隔n层的栈的基址。
interpret
这个函数的主要作用就是翻译,对目标代码解释执行。(这一部分没怎么学过,有一部分是我猜的)。其中模仿c的函数调用,我猜测的pc就是下一条指令,汇编的eip,b就是ebp栈基址。
判断code中每个数组单元的类型
LIT
a入栈(a中类型不一定,有偏移,也有常量等)。
OPR
若为双元运算则栈顶与次栈顶的内容进行运算,若为单元运算,则栈顶运算,结果放次栈顶。
LOD
变量的值入栈
STO
将栈顶的值赋给变量
CAL
调用函数,入栈时,计算某一层基址,栈中先入该层的基址,再入上层基址,最后放入下一条指令,之后将b变成本层基址。
INT
为栈开辟空间
JMP
跳转到指向的指令
JPC
若栈顶为零,则跳转到目标指令
main
至此,词法分析、语法分析和代码执行全部分析完毕。main函数就是打开输入、输出文件,然后对输入文件进行词法、语法分析生成.out文件,然后将其执行。
编译器过程分析
词法分析
我们可以通过代码得到以下正规式,改编译器就是通过这些正规式进行词法分析的。
reword -> BEGIN|END|IF|THEN|WHILE|DO|CALL|CONST|VAR|PROCEDURE
op -> +|-|*|/
assign -> :=|=
relop -> <|<=|>|>=|<>
number -> [0-9]+
letter -> [a-zA-z]+
id -> letter (letter|number)
boundary -> ,|.|;|(|)
这就是每个词法单元的正规式。
语法分析
我们可以通过代码得到以下文法:
block -> 声明 过程量声明 block statement|声明 statement
expression -> add term add term|term add term
term -> factor mul factor | factor
factor -> id|num|(expression)
statement -> if|call|do_while|set_value
condition -> ODD expression|expression relop expression
if -> IF condition THEN statement
call -> CALL id
do_while -> WHILE condition DO statement
set_value -> id := expression
add -> +|-
mul -> *|/
relop -> =|<|<=|>|>=
常量声明 -> CONST 量 ;
变量声明 -> VAR 量 ;
过程量声明 -> PROCEDURE 量 ;
量 -> id|id,量
声明 -> 常量声明|变量声明
这是一个LL(1)文法,系统采用了递归下降的预测分析方法来分析语法。
添加功能
注释
注释是编程的好帮手,但是整套源码里并未涉及对注释的识别,在词法分析时,可以直接将注释去掉。
我们先建立一个测试的程序:
var x,y;
procedure fun;
var a,b,c;
begin
a:=1;
b:=2;
c:=3;
x:=a+b;
end;
begin
call fun;
end.
这是一个符合上述程序语法规范的程序,如果在其中加入注释,就会报错,现在我们在getch函数的while循环中加入这样的语句:
while ( (!feof(infile)) // added & modified by alex 01-02-09
&& ((ch = getc(infile)) != '\n'))
{
if(ch=='#') // added by rainbow 06-21-18
{
while((ch = getc(infile)) != '\n');
break;
}
fprintf(outfile, "%c", ch);
printf("%c", ch);
line[++ll] = ch;
} // while
后记
添加功能很仓促,因为比较忙,等空闲下来再向深搞吧,玩个坑,哈哈。