Lexical Analysis and grammatical analysis.
把关键字、算符、界符称为语言固有的单词,标识符、常量称为用户自定义的单词。为此设置三个全程量:SYM,ID,NUM 。
SYM:存放每个单词的类别,为内部编码的表示形式。
ID:存放用户所定义的标识符的值,即标识符字符串的机内表示。
NUM:存放用户定义的数。
GETSYM要完成的任务:
滤掉单词间的空格。
识别关键字,用查关键字表的方法识别。当单词是关键字时,将对应的类别放在SYM中。如IF的类别为IFSYM,THEN的类别为THENSYM。
识别标识符,标识符的类别为IDENT,IDRNT放在SYM中,标识符本身的值放在ID中。关键字或标识符的最大长度是10。
拼数,将数的类别NUMBER放在SYM中,数本身的值放在NUM中。
拼由两个字符组成的运算符,如:>=、<=等等,识别后将类别存放在SYM中。
打印源程序,边读入字符边打印。
由于一个单词是由一个或多个字符组成的,所以在词法分析程序GETSYM中定义一个读字符过程GETCH。
PL/0编译程序采用一遍扫描的方法,所以语法分析和代码生成都有在BLOCK中完成。BLOCK的工作分为两步:
说明部分的处理:
说明部分的处理任务就是对每个过程(包括主程序,可以看成是一个主过程)的说明对象造名字表。填写所在层次(主程序是0层,在主程序中定义的过程是1层,随着嵌套的深度增加而层次数增大。PL/0最多允许3层),标识符的属性和分配的相对地址等。标识符的属性不同则填写的信息不同。
所造的表放在全程量一维数组TABLE中,TX为指针,数组元素为结构体类型数据。LEV给出层次,DX给出每层的局部量的相对地址,每说明完一个变量后DX加1。
例如:一个过程的说明部分为:
const a=35,b=49;
var c,d,e;
procedure p;
var g;
对它的常量、变量和过程说明处理后,TABLE表中的信息如下:
对于过程名的ADR域,是在过程体的目标代码生成后返填过程体的入口地址。
TABLE表的索引TX和层次单元LEV都是以BLOCK的参数形式出现,在主程序调用BLOCK时实参的值为0。每个过程的相对起始位置在BLOCK内置初值DX=3。
(b) 语句处理和代码生成
对语句逐句分析,语法正确则生目标代码,当遇到标识符的引用则去查TABLE表,看是否有过正确的定义,若有则从表中取出相关的信息,供代码生成用。PL/0语言的代码生成是由过程GEN完成。GEN过程有三个参数,分别代表目标代码的功能码、层差、和位移量。生成的目标代码放在数组CODE中。CODE是一维数组,数组元素是结构体类型数据。
PL/0语言的目标指令是一种假想的栈式计算机的汇编语言,其格式如下:
其中f代表功能码,l代表层次差,a代表位移量。
目标指令有8条:
① LIT:将常数放到运栈顶,a域为常数。
② LOD:将变量放到栈顶。a域为变量在所说明层中的相对位置,l为调用层与说明层的层差值。
③ STO:将栈顶的内容送到某变量单元中。a,l域的含义与LOD的相同。
④ CAL:调用过程的指令。a为被调用过程的目标程序的入中地址,l为层差。
⑤ INT:为被调用的过程(或主程序)在运行栈中开辟数据区。a域为开辟的个数。
⑥ JMP:无条件转移指令,a为转向地址。
⑦ JPC:条件转移指令,当栈顶的布尔值为非真时,转向a域的地址,否则顺序执行。
⑧ OPR:关系和算术运算。具体操作由a域给出。运算对象为栈顶和次顶的内容进行运算,结果存放在次顶。a域为0时是退出数据区。
编译结束后,记录源程序中标识符的TABLE表已退出内存,内存中只剩下用于存放目标程序的CODE数组和运行时的数据区S。S是由解释程序定义的一维整型数组。解释执行时的数据空间S为栈式计算机的存储空间。遵循后进先出的规则,对每个过程(包括主程序)当被调用时,才分配数据空间,退出过程时,则所分配的数据空间被释放。
为解释程序定义四个寄存器:
I:指令寄存器,存放当前正在解释的一条目标指令。
P:程序地址寄存器,指向下一条要执行的目标指令(相当于CODE数组的下标)。
T:栈顶寄存器,每个过程运行时要为它分配数据区(或称为数据 段),该数据区分为两部分。
静态部分:包括变量存放区和三个联单元。
动态部分:作为临时工作单元和累加器用。需要时临时分配,用完立即释放。栈顶寄存器T指出了当前栈中最新分配的单元(T也是数组S的下标)。
B:基地址寄存器,指出每个过程被调用时,在数据区S中给出它分配的数据段起始地址,也称为基地址。每个过程被调用时,在栈顶分配三个联系单元。这三个单元的内容分别是:
SL:静态链,它是指向定义该过程的直接外过程运行时数据段的基地址。
DL:动态链,它是指向调用该过程前正在运行过程的数据段的基地址。
RA:返回地址,记录调用该过程时目标程序的断点,即当时的程序地址寄存器P的值。
具体的过程调用和结束,对上述寄存器及三个联系单元的填写和恢复由下列目标指令完成。
INT 0 a
a:为局部量个数加3
OPR 0 0
恢复调用该过程前正在运行过程(或主程序)的数据段的基地址寄存器的值,恢复栈顶寄存器T的值,并将返回地址送到指令寄存器P中。
CAL l a
a为被调用过程的目标程序的入口,送入指令地址寄存器P中。
CAL指令还完成填写静态链,动态链,返回地址,给出被调用过程的基地址值,送入基址寄存器B中。
我所实现的PL0语言的编译程序,采用一次扫描,但不是边扫描边同时进行词法语义分析、语法分析,而是分开进行。扫描源文件时进行词法分析,生成的字母表(另外也将字母表存到文件里)。然后根据字母表数组进行语法语义分析,期间产生符号表用于生成代码,语法语义分析后的结果是得到了语法树和目标代码(同样的,也将语法树、符号表、目标代码存到了文件里)。解释执行只需要目标代码即可,通过保存的目标代码,对栈进行操作从而得到运行结果。
提供简单的报错功能,当词法分析出错时,程序停止并显示出错位置与出错对象。当语法分析错误时,程序停止并显示出错行数与出错原因。如果均没有错误则按照词法分析、语法语义分析、解释执行的正常步骤运行。
构建词法分析类LexicalAnalysis,类内方法getsym是边扫描边进行词法分析并将得到的词法分析单词表保存到全局变量Lexword型数组word中,其中Lexword是一个结构体,表示一个单词的信息如名称、类型、所在行数。调用getSym时,它从源程序中获得一个字符。如果这个字符是字母,则继续获取字符或数字,最终可以拼成一个单词,查保留字表,如果查到为保留字,则把sym变量赋成相应的保留字类型值;如果没有查到,则这个单词应是一个用户自定义的标识符(可能是变量名、常量名或是过程的名字),把sym置为id,把这个单词存入id变量。如果获得的字符是数字,则继续获取数字,并把它们拼成一个整数,然后把sym置为number,并把拼成的数值放入num变量。如果识别出其它的符号(算符与界符),则把sym则成相应的类型。如果遇到不合法的字符,把词法分析程序停止并显示错误。
构建语法分析类GrammaAnalysis,对词法分析得到的单词表进行处理从而产生符号表、语法树、目标代码。采用自顶向下的递归子程序法,语法分析的同时也根据语义生成相应的代码,并提供了简单的出错处理机制。根据PL0语法的规则,设计了以下函数:分程序分析过程(SubProgram)、常量定义分析过程(Const)、变量定义分析过程(Varible)、语句分析过程(Statement)、表达式处理过程(Expression)、项处理过程(Term)、因子处理过程(Factor)和条件处理过程(Condition)以及常量定义过程(ConstantDefinition)、过程头分析过程(ProcedureHeader)、赋值语句分析过程(AssignmentStament)、循环语句分析过程(LoopStament)、读语句分析过程(ReadStament)、写语句分析过程(WriteStatement)、过程调用分析过程(ProcedureCallStament)、复合语句分析过程(CompundStament)等。这些过程在结构上构成一个嵌套的层次结构,引入Tree类用于构造语法树,引入Codes类用于代码生成,根据语法不断递归调用各个过程的同时,构造语法树,并且生成目标代码,错误报告没有集结成类而是分散在各个递归调用的函数中。
构建解释执行类Execute,解释执行目标代码。目标代码是一个栈式语言,因此解释执行时用数组模拟栈的运行来执行目标代码,根据书上的知识,过程调用时,返回地址入栈、直接上层基址入栈(静态链)、原基址内容入栈(动态链),过程返回时,解释程序通过返回地址恢复指令指针的值到调用前的地址,通过当前段基址恢复数据段分配指针,通过动态链恢复局部段基址指针,实现子过程的返回,难点就是过程的调用与过程的返回。