pl0编译器源码

分析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)文法,系统采用了递归下降的预测分析方法来分析语法。

添加功能

注释

注释是编程的好帮手,但是整套源码里并未涉及对注释的识别,在词法分析时,可以直接将注释去掉。

我们先建立一个测试的程序:

1
2
3
4
5
6
7
8
9
10
11
12
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循环中加入这样的语句:

1
2
3
4
5
6
7
8
9
10
11
12
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
感谢老爷打赏
显示 Gitment 评论
undefined