目录
1.参考编译器介绍
笔者主要参考了PL/0语言的编译器,在《编译技术》一书的第17章对此有详细介绍。PL/0编译程序采用一遍扫描,以语法分析程序为核心,由其调用词法分析程序、错误处理程序,并在语法分析的同时进行语义分析,最后生成目标指令。
图1.1 PL/0编译系统图示
PL/0语言使用PASCAL语言书写的,整个编译程序(包含主程序)由18个过程或函数组成,这些过程和函数形成了嵌套的程序结构。
图1.2 PL/0程序构成
下面通过表格对18个函数或过程的作用进行简要说明。
过程或函数名 | 功能简要说明 |
p10 | 主程序 |
error | 出错处理,打印出错位置和错误编码 |
getsym | 词法分析,读取一个单词 |
getch | 漏掉空格,读取一个字符 |
gen | 生成目标代码,并送入目标程序区 |
test | 测试当前单词符是否合法 |
block | 分析程序处理过程 |
enter | 登录名字表 |
position(函数) | 查找标识符在名字表中的位置 |
constdeclaration | 常量定义处理 |
vardeclaration | 变量定义处理 |
listcode | 列出目标代码清单 |
statement | 语句部分处理 |
expression | 表达式处理 |
term | 项处理 |
factor | 因子处理 |
condition | 条件处理 |
interpret | 对目标代码的解释执行程序 |
base(函数) | 通过静态链求出数据区的基地址 |
表1.1 PL/0程序过程(函数)说明表
1.1词法分析
PL/0的词法分析程序 getsym 作为一个独立的子程序(过程)由语法分析程序调用,它的主要功能如下:
(1)跳过源程序中的空格字符;
(2)从源程序正文字符序列中识别出单词符号,并把该单词符号的类别以相应枚举值的形式(即内部编码)送人变量 sym中;
(3)用变量id存放标识符,用二分法查找保留字表,识别诸如 begin、end等保留字;
(4)如取来的单词为无符号整数,则将该整数数字字符串转换为整数值存入变量num中。
getsym 调用getch 扫描输入的源程序,取来一个个字符。getch 实际上是将输入(input)文件上的每一行源程序先读入缓冲区 line 中,然后再从 line 中取出字符。程序用变量ll记录当前源程序行的长度,用变量 cc 对当前所取字符在该行中的位置进行计数。getch 除取来下个字符外,还完成:
(1)识别并越过行结束信息;
(2)把从input文件读入的源程序同时输出到output文件上,以形成被编译源程序的清单(用户可以在终端屏幕上看到编译扫描的进程);
(3)在输出每一条源程序的开始处打印出编译生成的目标指令行号。
PL/0的语法分析采用了所谓“单符号先行”的技术,即在进入某语法成分的分析子程序前,一定要先读取一个单词符号放在 sym中。与此相应,在getsym中,也总是先读取一个字符放在变量 ch 中。因此,整个编译过程是先读一个单词符号加一个字符。
1.2语法分析
PL/0采用了自顶向下的递归子程序法进行语法分析,即为每个语法成分都编写了一个子程序,根据当前读取的符号,可以选择相应的子程序进行语法分析。
为了对PL/0程序进行语法分析,各分析子程序之间必定存在相互调用的关系,这种调用关系可以用下图来表示,称为PL/0的调用图。
图1.3 PL/0调用图
语法分析从读入第一个单词开始由非终结符‘程序’即开始符出发,沿上图箭头方向进行分析。
当遇到非终结符时,即调用相应的处理过程,也就是进入了另一个语法单元,再沿所进入的语法成分的箭头方向进行分析。
当遇到终结符时,则判断当前读入的单词是否与终结符相匹配,若匹配,则执行相应的语义程序(就是翻译程序),再读下一个单词继续分析。
遇到分支点时将当前的单词与分支点上的多个终结符逐个相比较,若不匹配时可能是进入下一个非终结符语法单元或是出错。
如果一个PL/0语言的单词序列在整个语法分析中,都能逐个得到匹配,直到程序结束符‘.’,这就说所输入的程序是正确的。对于正确的语法分析作相应的语义翻译,最终得到目标程序。
语法分析的核心过程是在block分程序中进行的。实际的语法分析工作主要分为两部分:说明部分和语句部分。
1.2.1对说明部分的分析处理
这一部分主要处理常量定义,变量说明和过程说明。对合法的常量名、变量名和过程名,应把它们的名字及有关属性信息通过enter 过程一一登录到符号表 tab 中,tab 是一个带变体的记录数组,表的每个登记项由名字name 种类 kind值 val(当 kind为常量时)、层次 level 和地址 adr(当 kind 为变量或过程时)所组成。
符号表每项记录中的 level 域应填入说明该变量名或过程名的分程序的层次。主程序的层次为0,各嵌套分程序的层次随嵌套深度递增PL/0 程序限制 level的最大值。
符号表中的 adr 域应填入每层局部变量所分配单元的相应地址(其起始值为 3,由地址分配索引变量 dx 指定)。对过程名,则应填入编译该过程所生成的 P-code指令序列的入口地址。
1.2.2对语句部分的分析处理
进入对语句的分析后,只要根据当前读入的符号(应是标识符或其他相应保留字)就可以立即断定当前的语句属于哪一种,便可以选择相应的分析程序进行分析。从语法上要对语句逐句逐词分析。当语法正确时就生成相应语句功能的目标代码。当遇到标识符的引用时就调用position函数查符号表tab,看是否有过正确的定义,若已有,则从表中取相应的信息,供生成代码用,否则出错。
1.3目标代码生成
PL/0源程序经过编译得到了一个假想的计算机的目标代码,即 P-code 指令代码。P-code 指令不依赖于任何实际的计算机,它的指令集极为简洁,指令格式也非常单纯,形如f l,a。其中,f为操作码(用枚举值表示);l为变量或过程被引用的分程序与说明该变量或过程的分程序之间的层次差;a对于不同的指令有不同的含义,可以是常数值、位移量、操作符代码等,具体如下表所示。
指令f | 层次差l | a | 关于l的说明 | 关于a的说明 | 操作含义 |
LIT | 0 | a | a为常量 | a进栈 | |
LOD | l | a | l为调用层与说明层的层次差 | a为变量在所说明层中的相对位置 | 变量进栈 |
STO | l | a | l为调用层与说明层的层次差 | a为变量在所说明层中的相对地址 | 取栈顶内容存入变量 |
CAL | l | a | l层次差 | a为被调用过程的入口地址 | 调用过程 |
INT | 0 | a | a为开辟的单元个数 | 栈顶指针增加(被调用的过程在栈中开辟数据区) | |
JMP | 0 | a | a为转向地址 | 无条件转移 | |
JPC | 0 | a | a为转向地址 | 条件转移(栈顶布尔值非零时转移) | |
OPR | l | a | l为层次差 | a为操作符编码 | 栈顶与次栈顶的内容进行运算,结果进栈 |
表1.2 P-code指令集
P-code指令可通过解释执行程序 interprel 使其在 PL/0计算机上运行,因此P-code 解释执行程序也可以看作是 PL/0 计算机的算法描述。PL/0计算机可看作由两个存储器,一个指令寄存器和三个地址寄存器组成。存储器code(实际由数组实现),用来存放生成的 P-code 指令,P-code 指令在解释执行过程中始终保持不变,因此它可以看成是一个只读存储器。数据存储器S实际是一个堆栈,用来动态分配程序的数据空间(栈式动态存储分配)。PL/0计算机没有专供运算的寄存器,因此,所有的算术运算都在数据堆栈栈顶的两个单元之间进行,并用运算结果取代原来的两个运算对象而保留在栈顶里。栈顶单元的地址(下标)放在栈顶地址寄存器T中,所以T作为栈顶指针,永远指向数据栈 S 的栈顶。指令寄存器I存放当前要执行的 P-code 指令。程序地址寄存器 P 存放下一条要执行的指令地址,当程序顺序执行时,每执行完一条指令后,P 中的地址值应加 1。还有一个基地址寄存器 B,它用来存放当前运行的分程序数据区在数据栈 S 中的起始地址。
当程序运行进入某一个过程(分程序)后,即在数据栈 S中为其分配一个数据区,运算操作就在该数据区上面的栈顶单元之间进行。若程序在当前过程中要调用其他过程(也可以是递归调用本身过程),便在当前数据区上面再叠加分配一个新过程的数据区·.···这样下去,直到某一个过程运行结束后,便从数据栈S中撤销相应的数据区。为了使过程运行结束后能恢复到原来调用处的运行环境,用 RA(返回地址)单元来保存应返回的调用点的指令地址,用 DL(动态链)单元来保存原调用过程的数据区的起始地址(基地址)。为此,才设立了一个基地址寄存器B来保存最新分配的数据区起始地址,B永远指向动态链的链头。
PL/0每个过程有自己的局部变量,而采用栈式动态存储分配不可能在编译时就知道该变量在数据栈中的绝对地址。因此,编译时只能确定该变量在它所在分程序数据区中的相对位置,即它相对于本分程序数据区的起始地址(基地址)的位移。考虑到在嵌套分程序的内分程序中可以引用外分程序或主程序中定义的变量,所以还必须在各分程序数据区中设立一个 SL(静态链)单元,用来保存它的直接外层分程序数据区的基地址,以便在引用外层说明的变量时,可通过静态链来找到该变量在 S 数据栈中的确切位置。在编译时,凡生成的涉及存、取变量的指令(如 LOD、STO、RED)时,其l值为引用该变量的分程序层次与说明该变量的分程序层次之差。这样,只要按层次差值l,沿着静态链往回找l次,即可找到说明该变量的分程序数据区的基地址,再加上相对地址值确定了该变量在数据栈 S中的“绝对”位置。
图1.4 PL/0数据栈
编译程序的目标代码是分析程序体时生成的,在处理说明部分时并不生成代码,而当分析程序体的每个语句时,语法正确则调用目标代码生成过程生成与PL/0语句等价功能的目标代码,直到编译正常结束。在经过语法、语义分析后,P-code 的生成是非常直观的,有关表达式的处理都按照逆波兰表示法(后缀表达式)生成相应的运算指令。
PL/0语言的代码生成是由gen完成。gen有三个参数,分别代表目标代码的f,l,a,gen 过程只是简单地把生成的P-code 指令送人 code 指令存储区。cx 是指令索引指针,用来表示下一条要生成的指令的地址。在生产某些转移指令(如JMP、JPC)时,往往不能马上确定转移地址 a的值,这时可以把这条指令的cx 值保持起来,等后面确定了转移地址以后,再根据所保持的 cx 值把转移地址返填回去。
由于PL/0程序具有嵌套的分程序结构,所以编译进入分程序后生成的第一条指令为无条件转移指令(JMP)。这条指令是为了跳过它所包含的其他分程序所生成的指令序列,而直接进入程序体中语句部分所生成的指令。语句部分生成的指令序列的第一条指令为INTOdx其作用是实现在数据栈 S 中为本分程序的数据区动态分配存储空间。
2.MIPS编译器总体设计
在参考了PL/0编译器的架构,并对编译器的各部分的实现方式进行了审慎思考之后,笔者做出了以下的初步设计。
本文所设计的编译器扫描三遍,第一遍进行词法分析,第二遍进行语法分析并生成中间代码,第三遍生成MIPS目标代码。编译系统的整体设计如下图所示:
图2.1 编译器设计图示
笔者采用Java语言来实现本编译器,类的设计则如下图设计。部分类没有完全展示,在图中以数字标明。
图2.2 编译器程序类的结构
Compiler是我们的程序入口,其读入源程序的每一行,并交由词法处理程序SentenceHandler处理,SentenceHandler进一步调用WordHandler的方法对每个单词进行识别,将最后的结果利用One_Word类储存起来。
待词法分析结束后,开始由CompUnit用自顶向下的方式进行语法分析。在这一部分中,为每个语法成分编写了一个类,因为数量较多,在图中按语法成分类型进行了分类后的简要展示,分为了FuncDef、Decl、Block、Exp这四个部分。因为SysY 语言的文法较复杂,所以在语法分析中并没有采用有限自动机,但是笔者设计时仍希望能够实现一定程度上的自动解析,减小代码复杂度,故所有语法成分继承自同一父类Element,以使用共通的方法。在生成中间代码的过程中,还使用了TempResult、LabelControl、ConstTable、WordValue等类。
语法分析结束后,Compiler再读入中间代码,将每一行交由CreateMIPS进行处理,这一过程中使用到了符号表WordTable以及其他一些辅助类。
为了输出词法分析结果、语法分析结果、中间代码、目标代码、错误处理结果到文件中,采用了Printer类专门进行输出工作。
3.词法分析
3.1整体设计
词法分析工作类SentenceHandler首先进行对源程序语句进行prehandle,该方法可以实现以下几个功能:
- 跳过程序中的注释;
- 识别源程序中的字符串常量;
- 去掉语句中的空字符并分割;
- 将识别出的字符串常量和分割后的字符串递交getWords方法。
在prehandle对源程序语句进行了分割和去冗的基本处理后,由getWords调用WordHandler的方法对每个单词进行识别。
表3.1 SysY单词类别码表
WordHandler中提前存储了单词字符串和类别码(枚举值)的对应关系,对于每个接收的字符串,进行字符串遍历,最后得到分类。若字符串未遍历到结尾,说明原先的字符串并未进行最细分割,则在此处同样完成单词分割的工作,最后将识别的结果以One_Word类型(同时存储字符串和类别码),按顺序存入wordList——即存储词法分析结果的列表中。这样,词法分析步骤就结束了。
3.2局部修改
1.因为采取了逐行读入源程序的读取方式,而上一行的程序内容在词法上亦可能对下一行造成影响。为达到SentenceHandler中prehandle所期望的功能,用了两个状态量来标明是否正处于识别字符串或注释的状态。
2.在WordHandler类的getType方法中分析单词类型时,由于整型常量和标识符的对应字符串并不固定,所以在其他单词类型的分析之后,根据字符串的构成是否符合这二者的构成法单独进行了分析。
4.语法分析
4.1整体设计
词法分析结束(即源程序已经全部经过经过分析并存入wordList)后,由CompUnit的check方法进入语法分析过程。本编译器采用自顶向下的分析方法进行最左推导。各语法成分的分析子程序存在相互调用的关系,各语法成分类重载了父类的checkSimilar方法,该方法用以进行改语法成分的匹配过程,返回值为成功匹配语法成分的单词数量,用以增大取单词的索引值index。每个语法成分的分析子程序内,根据该语法成分的产生式规则进行从左至右的匹配。若遇到有分支的情况,则向前查看多个单词,直到可以消除二义性,进入确定的唯一分支。若遇到产生式右侧某个语法成分的数量不确定的情况,则用条件循环尝试进行不定次数的匹配。如此反复,直到单词序列逐个得到匹配,到达wordList尾部。
生成中间代码的部分可以分为:条件语句、循环语句、函数调用、函数声明、变量声明、常量声明、运算、赋值等,只要根据中间代码的规定进行对应语句的转化就行,下面对一些进行了特殊处理的部分进行详细说明。
中间代码生成严格按照三元式的要素,对于复杂计算式和数组索引值等,生成临时变量存储计算结果,保证中间代码每行语句的简洁,由TempResult来控制这一过程。由于这一设置,需要在计算式中某一语法成分的分析完成后,返回给上一级储存该组成部分计算结果的值。对于一维数组、二位数组、整型的声明,也各单独设置了中间代码的生成方法,数组声明的赋值会将等式右边按‘{’、‘}’进行切割,逐一赋给数组元素。函数声明在FuncDef的分析过程中可以顺利完成,对于数组型参数,数组维度同样需要获得储存ConstExp分析结果的临时变量。
对于if和while的条件式,根据短路规则,其有着较为复杂的分支,为此,设置了LabelControl类,并设置三个属性startLabel、endLabel、nextLabel,其含义如下表所示。
属性 | 条件式 | if/while语句 |
startLabel | 条件式开始 | 语句的开始 |
endLabel | 不满足该条件式,下一个执行语句的位置 | if/while代码块结束 |
nextLabel | 满足该条件式,下一个执行语句的位置 | 满足条件,进入代码块内部执行的位置 |
表3.2 label含义
生成中间代码的过程中,用label来控制跳转关系,并按一定的规则命名各种label。可知在LOrExp语法成分中,每一个直接构成的语法成分的nextLabel是LOrExp的nextLabel,endLabel是下一个条件式的startLabel(若不存在下一个条件式,则是LOrExp的endLabel);在LAndExp中,每一个直接构成的语法成分的endLabel是LAndExp的endLabel,nextLabel是下一个条件式的startLabel(若不存在下一个条件式,则是LAndExp的nextLabel)。而在Stmt语法成分的分析中,若进入if语句和while语句对应的产生式规则的分支,则在适当的地方打印if/while语句的startLabel、endLabel和nextLabel。
在文法规则中,还存在以“!”、“>”、“<”等符号进行计算的条件表达式,对于比较符号,统一按照三元式进行组织,如“[临时变量]=2>=1”因为C语言中,对于逻辑正确的比较式赋值为1,逻辑错误的比较式赋值为0,因此按照三元式组织可以正确且便捷地处理比较式。而对于“![式子]”,则将其等价的转化为了“[式子]==0”来进行计算。
4.2局部修改
1. 在最终完成的代码中,对于极少数语法成分(如BType、UnaryOp)没有单独建立类,直接在产生式左边语法成分的子程序中进行了分析。
2.在给出的文法规则中存在着多条直接左递归规则,如:AddExp → MulExp | AddExp ('+' | '−') MulExp
由于LOrExp、LAndExp、EqExp、RelExp等多个语法成分也有非常相似的产生式规则,为了降低代码冗余,使语法分析程序的结构更清晰,在父类中用innerCircle方法将有着左递归规则的语法成分抽象出来统一处理。
3.文法规则中说明了ConstExp的推导中,Ident必须为常量,但这一要求在AddExp等语法成分的原来的分析过程中并没有体现,因而,给各语法成分的check方法新增参数——标明是否需要为常量值的bool值,原来计划在ConstDef的分析过程中利用常量表ConstTable记录常量,后由于错误处理的需要,在语法分析的过程中用ExeStack建立了符号栈记录所有的常量、变量、函数,所以直接利用该符号栈记录。
4.用label对跳转进行控制的时候,存在原来设计的属性不足以处理的特殊点。if语句特殊之处是存在由else引导更多条件分支的可能,所以需要增加这一整体的endLabel,在成功进入某一个条件分支并运行完代码块后,跳转至该endLabel。而while语句的特殊之处则是在代码块的最后需要输出跳转到while语句的startLabel的指令。
5.由比较式计算的设计可知,编译器对于条件式的计算也变成了条件式和0的等值比较,因此在中间代码中,条件跳转最后全由“cmp [变量],0”、“beq(或bne) Label”收尾,而不出现bgt等其他比较指令。
5.错误处理
5.1整体设计
需要处理的所有错误类型如下表所示。
错误类型 | 错误类别码 | 解释 | 对应文法及出错符号(…省略该条规则后续部分 |
非法符号 | a | 格式字符串中出现非法字符报错行号为<FormatString>所在行数。 | <FormatString> → ‘“‘{<Char>}’”’ |
名字重定义 | b | 函数名或者变量名在当前作用域下重复定义。注意,变量一定是同一级作用域下才会判定出错,不同级作用域下,内层会覆盖外层定义。报错行号为<Ident>所在行数。 | <ConstDef>→<Ident> … <VarDef>→<Ident> … |<Ident> … <FuncDef>→<FuncType><Ident> … <FuncFParam> → <BType> <Ident> ... |
未定义的名字 | c | 使用了未定义的标识符报错行号为<Ident>所在行数。 | <LVal>→<Ident> … <UnaryExp>→<Ident> … |
函数参数个数不匹配 | d | 函数调用语句中,参数个数与函数定义中的参数个数不匹配。报错行号为函数调用语句的函数名所在行数。 | <UnaryExp>→<Ident>‘(’[FuncRParams ]‘)’ |
函数参数类型不匹配 | e | 函数调用语句中,参数类型与函数定义中对应位置的参数类型不匹配。报错行号为函数调用语句的函数名所在行数。 | <UnaryExp>→<Ident>‘(’[FuncRParams ]‘)’ |
无返回值的函数存在不匹配的return语句 | f | 报错行号为‘return’所在行号。 | <Stmt>→‘return’ {‘[’Exp’]’}‘;’ |
有返回值的函数缺少return语句 | g | 只需要考虑函数末尾是否存在return语句,无需考虑数据流。报错行号为函数结尾的’}’所在行号。 | FuncDef → FuncType Ident ‘(’ [FuncFParams] ‘)’ Block MainFuncDef → 'int' 'main' '(' ')' Block |
不能改变常量的值 | h | <LVal>为常量时,不能对其修改。报错行号为<LVal>所在行号。 | <Stmt>→<LVal>‘=’ <Exp>‘;’|<LVal>‘=’ ‘getint’ ‘(’ ‘)’ ‘;’ |
缺少分号 | i | 报错行号为分号前一个非终结符所在行号。 | <Stmt>,<ConstDecl>及<VarDecl>中的';’ |
缺少右小括号’)’ | j | 报错行号为右小括号前一个非终结符所在行号。 | 函数调用(<UnaryExp>)、函数定义(<FuncDef>)及<Stmt>中的')’ |
缺少右中括号’]’ | k | 报错行号为右中括号前一个非终结符所在行号。 | 数组定义(<ConstDef>,<VarDef>,<FuncFParam>)和使用(<LVal>)中的']’ |
printf中格式字符与表达式个数不匹配 | l | 报错行号为‘printf’所在行号。 | Stmt →‘printf’‘(’FormatString{,Exp}’)’‘;’ |
在非循环块中使用break和continue语句 | m | 报错行号为‘break’与’continue’所在行号。 | <Stmt>→‘break’‘;’|‘continue’‘;’ |
表5.1 错误类型
在编译的过程中,如果有错误出现,就调用错误处理程序,输出错误类型及出现位置,跳过错误并返回,继续进行编译,如果无法成功地跳过这个错误,则停止编译。其中,a类错误可以在词法分析阶段处理,i、j、j类错误会导致语法分析过程中出现错误,也可以直接处理。l类错误可以在语法分析过程中增加一步进行检验。而其他错误则需要新增一个数据结构才能处理。
所以,在语法分析过程中需要利用ExeStack类建立符号栈,将声明的变量、常量和函数记录进符号表中,并记录常量和变量的数据类型,函数的参数信息和返回值类型。同时,还用Region类记录程序块,保存程序块的父块、是否可以有返回值、返回值的情况是否满足、是否在循环体中、创立时的符号栈顶等信息。在一个程序块编译完成后,弹出该程序块中声明的变量及常量,这样其他类型的错误也可以得到解决。
5.2局部修改
因为错误处理板块的需求,对之前模块的类进行了一些修改。对于代表单词的One_word类,增加了出现行数line_number的属性,以在输出错误信息时使用。并增设了One_word的继承类Num和Function,分别表示值和函数,Num又有继承类Variable和Const,分别表示变量和常量。所有这些子类储存对应结构的相关信息。
图5.1 One_word关系类图
6.代码生成
6.1整体设计
MIPS代码分为数据段和代码段,我们需要在数据段声明所有的字符串常量,为此,新增ConstStr类,在语法分析过程中,将所有的字符串常量利用ConstStr类进行存储,并在开始代码生成时,顺序输出声明。数据段结束后,开始输出代码段,逐行读取之前生成的中间代码文件,在CreateMIPS中进行每条中间代码语句生成目标代码的过程。
先只进行最简单的考虑,数据全部存储在内存中,传参全部通过堆栈。每次声明新的变量/常量时按序分配内存中的地址,在MIPS中表示为相对gp寄存器(全局变量/常量)和fp寄存器(局部变量/常量)的偏移量。使用WordTable类维护一个符号表,并完成所有新增变量/常量、安排内存地址、获取已知变量/常量地址的工作。
对于加减乘除模运算,MIPS中有直接对应的运算符,在中间代码中则由三元式表示,则建立由三元式转化为MIPS的方法即可。而对于比较式来说,由于MIPS中只有“slt”这一个式子可以达到比较赋值的目的,因此将含“<”、“>”、“≤”、“≥”的三元式进行一定转化后变为slt命令,而对于“==”、“!=”来说,则需建立新的label,用bne或beq命令来跳转后赋值,为了防止对顺序执行的其他代码产生干扰,这部分新生成的辅助程序段统一在最后进行输出。所有的中间代码中的cmp语句和之后的bne/beq语句也可直接转化为MIPS中的bne/beq语句,只需要注意同时读入两句之后才能进行生成。
如语法分析和错误处理中考虑的一样,由于引用逻辑,需要在某一个block结束后,释放在这一block中声明的变量/常量,故此处用BlockFlag类来完成这一block的标识工作。
对于函数调用,需要进行现场的保存和恢复,因此需要记录当前函数开始时的符号栈顶,并建立保存现场、传参和恢复现场的接口。call语句等同于MIPS的jal命令。而对于函数的声明,则直接根据中间代码中para的顺序获取堆栈中的参数位置,并利用WordTable新增局部变量即可。
在MIPS程序中,需要使用寄存器进行计算,而不能直接用内存中地址进行计算,因此,上述过程中需要多次为值分配寄存器,本次设计的系统采用Register类来管理这一过程,Register管理寄存器8~15及24、25的分配以及释放。
6.2局部修改
1.在开始考虑生成中间代码的过程中,并没有考虑到block结束后符号栈弹出数据的过程,因而没有显著标识表明block开始,所以需要做出更改,用关键字标明block的开始和结束。
2.WordTable负责“获取变量/常量在内存中的位置”这一功能,MIPSWord负责存储变量/常量的地址。开始设计时,对于整型,返回值就是初始时分配的位置;对于数组,声明时记录数组在内存中首地址(以字符串表示);但是,由于函数传参时,数组传值为数组首地址,所以在声明时MIPSWord储存的是一个存储了数组首地址的内存地址。这种差异导致WordTable该功能实现出现了问题。
为了对获取数组元素位置的过程进行统一,对于数组,声明时记录指针寄存器和偏移量,在获取数组中元素的地址时,计算该元素相对于数组首地址的偏移,指针寄存器的值加上这两个偏移量合起来得到该元素的地址,该地址保存在一个临时寄存器中;对于函数传参,则将数组首地址与该元素相对于数组首地址的偏移进行加和即可。
3.由于设计中需要由编译器来控制内存分配,对于数组,需要直接获取到数组的真实长度,所以对编译器的语法分析部分进行了修改,对于所有常量值,进行了提前计算。