PL0编译器分析与语法扩展

时间:2022-06-20 07:04:30

转载请注明来自 b0t0w1’blog

一、简介

1.什么是 pl0 语言

  PL 语言是PASCAL语言的一个子集,该语言不太大,但能充分展示高级语言的最基本成分。PL0具有子程序概念,包括过程说明和过程调用语句。在数据类型方面,PL0只包含唯一的整型,可以说明这种类型的常量和变量。运算符有+,-,*,/,=,<>,<,>,<=,>=,(,)。说明部分包括常量说明、变量说明和过程说明。

2.pl0语言语法

  给个简单的例子,program 表明这是 pl0 语言, main 为程序名称,接下是变量声明与过程声明,beginend 中间为 main 的执行程序,注意的是声明必须在一开始就全部声明完毕。

program main;
var a: integer;
procedure P1 (a:integer);
begin
var b: integer;
b := a;
// 输出 b
call write(b)
end;
begin
// 输入 a
call read(a);
call P1(a)
end.

  需要注意的是每个程序块内如果只有一条语句,可以不加begin..end 结构,但是多条语句必须使用 begin…end结构。同时,除了最后一条语句外,每句都已 ‘;’ 结尾。 上面的 call write(b) 和 call P1(a) 为最后一句,不加分号。

3.pl0编译器

  编译器由两个项目工程组成, pl 工程将 pl0 语言翻译成中间语言, interpret 工程根据中间语言代码生成可执行文件。
  这次所使用的编译器已实现 while 循环和 if 判断语句,变量类型方面实现了整型和字符型以及数组,运算实现了加减乘除、赋值以及 bool 运算。但不支持 for 循环等语法。

4.功能扩展

  本次在 pl0 编译器原有的基础上添加了 for 循环 、 repeat 循环, case 条件选择、 +=赋值、 -=赋值、{}注释、–、++ 等。这些功能都是在 PL 工程下实现,本次实验不修改 interpret 工程。

二、 原编译器分析

1. 整体思路

本次分析只分析扩展功能时需要使用的代码,其他略过。
  interpret 工程用于翻译中间语言代码,此次不作修改、不作分析直接使用就好。 PL 工程中 pl.cpp 用于生成中间语言代码,其他 .cpp 用于 I/0 操作,亦不作分析。
  此次分析从各函数入手,着重分析扩展语法时需要用到,需要修改的函数。对函数的分析比较简要,想深入了解的欢迎下载上面链接的源代码,里面有更为详尽的分析。

2. 函数分析

  main() 函数中,首先输入 pl0 源程序,然后处理源程序输出中间语言代码。main() 函数虽短,但是调用了复杂的函数,函数之间嵌套频繁,个人觉得众多函数才是这个程序的精髓。由于篇幅,只分析需要用到的函数。

(1)initial()

  initial() 用于初始化,首先是保留字。保留字作为全局变量,应当在文件一开始就声明其个数与具体有哪些。原有为 20 个,此处为我添加后的,每一个对应一个关键字。

#include "ptoc.h"
/*
保留字个数
*/

const int norw = 27;
enum symbol {nul, ident, intcon, charcon, plus, minus, times, divsym, eql, neq, lss, leq, gtr, geq, ofsym, arraysym, programsym, modsym, andsym, orsym, notsym, lbrack, rbrack, lparen, rparen, comma, semicolon, period, becomes, colon, beginsym, endsym, ifsym, thensym, elsesym, whilesym, repeatsym, dosym, callsym, constsym, typesym, varsym, procsym, forsym, downtosym, tosym, untilsym, casesym, otherwisesym, addsym, subsym, last_symbol};

同时声明类型、指令等,注意此处不为关键字,只是为了方便本程序处理

enum oobject {konstant, typel, variable, prosedure, last_oobject};

enum types {notyp, ints, chars, bool_, arrays, last_types};

enum opcod {lit, lod, ilod, loda, lodt, sto, lodb, cpyb, jmp, jpc, red, wrt, cal, retp, endp, udis, opac, entp, ands, ors, nots, imod, mus, add, sub, mult, idiv, eq, ne, ls, le, gt, ge, last_opcod};

  然后我们看看 initial() 是怎么处理的。只列出扩展的代码,其余详见上面链接下载的工程源码。

  每个 word1[] 元素存放一个关键字,大小只有 10 个字节。这里我只列出扩展的关键字。有没有发现规律?没错,存放时按照大小顺序排列的,因为在查找中使用的是二分查找。
  
  后面的 wsym[] 用于把 word1[] 和前面声明的 symbol{} 对应起来, 注意必须是一一对应,下标顺序必须保持一致。
  然后是 ssym[]mnemonic[],记录着所有的运算符和中间语言指令。同样这些值必须在前面的 enum 处声明。

  扩展功能时,首先扩充前面枚举的值,然后扩充 initial() 的值,注意要分清楚扩充的类型,是关键字还是运算符或者是指令,在相应的地方修改。

void initial()
{
....
word1[ 5] = "case ";
word1[ 8] = "downto ";
word1[11] = "for ";
word1[17] = "otherwise ";
word1[20] = "repeat ";
word1[22] = "to ";
word1[24] = "until ";

wsym[ 5] = casesym;
wsym[ 8] = downtosym;
wsym[11] = forsym;
wsym[17] = otherwisesym;
wsym[20] = repeatsym;
wsym[22] = tosym;
wsym[24] = untilsym;
....
}

initial() 并没有结束,还要初始化一些集合,此处也比较难理解。不懂时略过以后慢慢体会。

  首先思考一个问题,保留字全部声明了,但是保留字的使用也要合乎一定的规则(此处规则不是指类似于 forwhile 循环内部的关键字合法性,而是将 forwhile 看成一个块)。具体来说,比如程序开始,读入 program main ; 确认是 pl0 语言,那么接下来的一定是 varconsttypeprocedure 等声明,而不可能是 forwhileif 等语句,这些语句应该在声明后且也在 begin 后才能出现。也就是说程序中出现就是语法错误,编译器应当报错。

  那如何判断读到的关键字在此处是否合法呢?initial() 集合吧关键字进行了分类,每个类构成一个集合,编译语句时要指明所属的集合,如果超过了说明程序语法错误或者这一块程序编译完需要更换集合。比如前面例子中读到了 begin 说明声明结束,要开始处理 for 等结构了。

详细如下:
decbegsys: 声明开始的符号集
facbegsys: 因子开始的符号集
typebegsys: 设置类型符号集
constbegsys: 常量申明符号集
statbegsys: 语句开始的符号集
  
  其中 statebegsys 因为扩展了 forrepeatcase 等结构,要将该结构的开始关键字放入,但是结构里的关键字不能加入。比如 for 循环里的 to 等,它们的语法是否合法有编译 for 的函数判断。

statbegsys = set_of_enum(symbol)::of(beginsym, callsym, ifsym, whilesym, forsym, repeatsym, casesym, eos);

其他集合内容详见代码。

  编译程序时首先要指定关键字的集合,如果不在集合里说明程序语法错误或者要更换集合,比如上面的例子里,都到了 begin , 意味着声明结束,开始处理 forif 等结构了,此时再出现 var 等声明语句就是错误的。initial() 函数还有一些初始化,不再分析。

废话这么多,也就是 first 集合和 follow 集合的思想。

(2)getch() 与 getsym()

下面介绍读入函数。
  getch() 是以字符为单位读入的,遇到空格跳过。每次执行完后读入一个字符到变量 ch 中。getsym() 是以符号位单位读入的,里面有调用 getch() 来完成这一操作,每次执行后读入一个符号到变量 sym 中。比如在编译

a := b

  赋值语句之前,调用 getsym(),读到了 ‘a’ , 那么 sym = ident, ident 在前面枚举时声明过,代表这是一个变量。

(3)simpleexpression() 和 expression()

  这两个函数都是处理表达式的值,所不同的是 simpleexpression() 函数用来计算简单表达式,如

a + b * c

expression() 用来计算比如大于等于,小于等于。。。等表达式。 如

a + 1 > b

  其实, expression() 函数中有调用 simpleexpression() 函数,用来计算操作数左右表达式的值。当你只需要计算加减乘除时调用 simpleexpression() 足矣。

  比如按上面的例子,想要处理 a + 1 > b 是真还是假的时候,首先 getsym(), 得到 a, 此时sym = ident。然后调用 expression()expression() 会调用 simpleexpression() 用来计算 a + 1。最后expression() 函数会多调用一次 getsym() 读到 b 之后的下一个符号。simpleexpression() 用法一模一样。

(4)factor() 和 term()

  这两个函数同样用于计算表达式,计算的表达式也更为简单。 嵌套关系为: simpleexpression() 调用 term()term() 调用 factor()factor() 用于计算因子,而 term() 用于计算项。
  举个例子:
  ++a * 2 + 5
  ++a 由 factor() 计算,* 2 由 term() 计算,+ 5 由 simpleexpression() 计算。因为加减是项与项之间的运算,而乘除是项内的运算,类似于 ++a 则是因子的计算。整个函数嵌套严密,分工明确,我们计算时只用调用 simpleexpression() 就可以了。如果要扩展 ++ 这样的功能,则要去修改 factor()

(5)position()

  position() 函数顾名思义就是寻找地址,也是众多函数中最简单的一个了。它用来返回声明的一个变量在 nametab[] 中的下表。如果返回值为 0 表示未找到,说明该变量未声明。用法见下面的 case 语句的扩展。

(6)assignment()

  assignment() 用来完成赋值操作,既可以是整型,也可以是字符型。用法类似于 expression(),比如赋值语句中:

a := 1

  只需先 getsym() 读到 ‘a‘, 然后执行 assignment() 即可完成赋值。跟 expression() 一样也会多读一个 sym

(7)block()

  block() 用来把一程序块翻译为中间语言代码,块的范围是指从声明到 begin….end 结束。也就是说 main()只需调用一次 block() 即可完成所有代码的翻译。那么 block() 函数的意思在哪里呢? 比如你声明一个 procedure, procedure 中的格式和 main 是一样的,也是先声明然后是 begin 语句,所以在 procedure 中也只需调用一次 block() 函数。

(8)statement()

  statement() 函数个人觉得是该编译器的核心,它在 block() 中被调用,用来具体实现代码的翻译,我们许多的扩展都是在这里修改。statement() 每调用一次只能执行一条语句的翻译,但这一条概念不是指一行,而是指一个结构,比如 beginendforwhile 等。虽然这些结构有好多代码,但每个结构只需一个 statement() 便可以解决。所以也就不奇怪为什么 block() 中只调用了一次 statement() 了。因为每个块都是由 begin….end 结构组成的,在 statement() 看来,这是一条语句。至于 begin….end 中使用了很多结构, 比如 forif、赋值等语句每一个都需要调用 statement()

  statement() 函数每识别了一个结构就会调用相应的翻译函数。比如识别了 begin,则会调用 compound() 函数,识别了 while ,则会调用 whilestatement() 函数…….上面所说的调用多条 statement() 就是在 compound() 中实现的,写的非常好,但在此就不讨论了。

(9)error()

error()用于告诉程序员错误类型,比如变量未声明,调用 error(10) 来实现。当然是实现规定好的。 

(10)其他函数

  还有很多函数,这里列举了不到三分之一,有些不会用到,而有些只是直接调用即可不用深入理解。上面着重介绍的也没有给大家分析源码,但建议大家还是看一下下列函数的源码,写的都非常好,深入分析一下会提升对问题的理解程度,真正明白编译器的工作机理。

  • compound()
  • getch()
  • getsym()
  • simpleexpression()
  • expression()
  • factor()
  • term()
  • block()
  • statement()
  • position()
  • assignment()
  • whilestatement()

虽然有很多,集中时间最多两天就可以看完,还是建议多多看一下。
函数的分析就到此结束,下面进行指令的简单介绍。

3.指令简介

1. 简介

  首先,指令的由 interpret 工程设计,本次并不修改该工程,所以不作详尽分析,只是列出使用的方法。
  为了方便使用,在 PL 工程中使用 gen() 函数边可以生成中间代码指令,比如 gen(add, 0, 0); 便是计算栈顶和次栈顶的加法值,具体过程是 add 指令需要两个操作数,因此弹栈两个,再把加法结果压栈。在使用指令时我们要把操作数放到栈顶和次栈顶。

三、扩展语法

1. for 循环

(1)准备工作

  在全局变量symbol中添加 forsym、tosym、dosym 关键字,然后在initial()函数中的word1[]和 wsym[]数组再次添加,注意这时需要按照ascii码顺序添加,因为在getsy()函数中查找使用了二分查找。最后在initial()函数中的集合statbegsys{}中添加 forsym,不添加tosym和dosym。因为forsym 是作为一个语句的开始符号,而dosym和tosym不可能出现在开始位置。然后在statement()函数中增加对读到 forsym 的判断,当读到 forsym就调用forstatement()函数,当然函数要提前声明。

(2)代码思路

for循环例子:
for a = 1 to 2+3 do ….

(1)首先判断 for 后面是不是变量
(2)执行赋值函数 assignment(), 进行 a = 1 的赋值
(3)赋值语句后面紧接着是保留字 to
(4)用 cx1 记下此时的 cx。
(5)执行函数 simpleexpression() 计算 b 表达式的值(2+3)
(6)调用指令 le, 判断当前 a 是否小于等于 b。 cx2 记下此时的 cx, 如果 a > b 用 jpc 指令跳出 for 循环,但跳出地址未知.
(7)紧接着是关键字 do。用 statement() 编译程序块,
(8)把 a 压入栈顶,执行 add 指令 +1, 使用 jmp 跳回到 cx1, 同时记下此时的 cx ,回填前面的 jpc 指令。然后执行第五步……直到跳出for。

2. 代码

首先在statement() 中增加判断,函数声明略过

else if (sym == forsym){
forstatement(level, fsys, cx1, cx2);
}

函数主体

void forstatement(int &level, symset &fsys, int &cx1, int &cx2){
item x;
getsym();

//判断是否为变量
if (sym == ident){
//assignment()中会读取下一个符号,下一个符号位 to, 必须把 tosym 加入集合中
assignment(i, level, fsys + set_of_enum(symbol)::of(tosym, eos));
}
else{
/*
如果 for 紧接着不是 ident, 为非法字符。
*/

error(0);
}
if (sym == tosym){
getsym();

/*
记录条件判断的位置。
*/

cx1 = cx;

/*
把循环变量取出放到栈顶
*/

A1 &with = nametab[i];
gen(lod, with.lev, with.adr);

/*
计算 to 后面的表达式
并读取 'do', 所以集合 fsys 扩充加入 dosym
*/

simpleexpression(fsys + set_of_enum(symbol)::of(dosym, eos), x, level);

/*
比较指令, 循环变量与to后面的表达式
*/

gen(le, 0, 0);

/*
保存循环结束点
循环结束点,跳出循环,地址未知。
*/

cx2 = cx;
gen(jpc, 0, 0);

if (sym == dosym){
getsym();
statement(fsys, level);

/*
处理循环变量
将循环变量地址压到栈顶
将循环变量压到栈顶
将步长压到栈顶
执行 add 命令, 弹出栈顶和次栈顶, 将 add 结果压到栈顶
执行 sto 命令, 弹出栈顶和次栈顶, 将栈顶的值存入次栈顶对应的地址中去,不再压栈
*/

A1 &with = nametab[i];
gen(loda, with.lev, with.adr);
gen(lod, with.lev, with.adr);
gen(lit, 0, 1);
gen(add, 0, 0);
gen(sto, 0, 0);

/*
跳回循环开始点
回填循环结束地址
*/

gen(jmp, 0, cx1);
code[cx2].a = cx;
}
else{
/*
缺少 do
*/

error(37);
}
}
else{
/*
缺少 to
*/

error(0);
}
}

2. repeat 循环

(1)准备工作

 在全局变量symbol中添加 repeatsym、untilsym关键字,然后在initial()函数中的word1[]和 wsym[]数组再次添加,同样要注意按顺序添加。最后在initial()函数中的集合statbegsys{}中添加 repeatsym, 然后在statement()函数中增加对读到 repeatsym 的判断,当读到 repeatsym 就调用repeatstatement()函数。

(2)代码思路

Repeat循环例子:

repeat
begin
......
end
until ....;

repeat 语句同 while 语句很类似,
(1)读到repeat后,用 cx1 记下此时的 cx, 然后运行 statement() 函数执行语句块。
(2)直到读到 until 时后执行 expression()函数判断bool表达式真假。
(3)使用 jpc 指令,如果条件为假跳回 cx1 执行。条件为真时跳出 repeat 循环向下执行。

2. 代码

void repeatstatement(int &level, symset &fsys, int &cx1){
/*
跳回循环执行
*/

cx1 = cx;
item x;
getsym();

/*
只需一条 statement 语句就可以,只有一条语句可以不加 begin end,
多条语句必须加入 begin end
*/

statement(fsys + set_of_enum(symbol)::of(semicolon, untilsym, eos), level);
if (sym == untilsym){
getsym();
expression(fsys + set_of_enum(symbol)::of(semicolon, eos), x, level);
if (x.typ != bool_) error(34);
gen(jpc, 0, cx1);
}
else{
/*
没有读到 until
*/

error(0);
}
}

3. case 判断

(1)准备工作

  在全局变量symbol中添加 casesym、otherwisesym关键字,然后在initial()函数中的word1[]和 wsym[]数组再次添加,同样要注意按顺序添加。最后在initial()函数中的集合statbegsys{}中添加 casesym。然后在statement()函数中增加对读到 casesym 的判断,当读到 casesym 就调用casetstatement()函数。

(2)代码思路

case循环例子:

 case k of
1 : b := 1;
2 : b := 2;
otherwise : b := 0
end;

(1)首先记下 a + c 表达式计算前的地址 cx1 = cx,考虑到k有可能是表达式然后计算 k 的值,再记下执行后的地址 cx2 = cx
(2)新建变量case_num = 0; 表示 case 语句情况的个数,每有一个 case 就执行 case_num++,
(3)每遇到一个 case 语句都要进行 k 值与 case 值的判断,判断前把 code[]数组中从cx2至cx3 的代码复制过来,即每遇到一个 case 都会执行一遍 cx2 至 cx3 的代码,即计算 k 的值。
(4)每个 case 语句执行完的地址都是相同的,都是用 jmp 跳出 case 语句,但是并不知道地址,用 address[100] 存放各 case 语句的 jmp 指令的地址,读到 end 后用 cx 回填 address[] 中存放的地址对应的 jmp 跳转地址。
(5)读到 otherwise 说明case值没有匹配上,执行otherwise后面的程序,直接调用 statement()。
(6)读到 end 表示结束。

2. 代码

void casestatement(int &level, symset &fsys, int &cx1, int &cx2) {
/*
因为 k 可能为表达式,
所以 cx3 和 cx2 一同用于保存 k值 计算前后的地址
*/

int cx3;

getsym();

/*
记录 k 值计算前的地址
*/

cx2 = cx;
item x;

/*
计算 k 值
*/

expression(set_of_enum(symbol)::of(ofsym, eos) + fsys, x, level);

/*
记录 k 值计算后的地址
*/

cx3 = cx;

/*
是保留字
*/

if (sym != ofsym) {
error(24);
}

/*
记录 case 语句中情况的个数
*/

int case_num = 0;

/*
记录各 case 语句中 jmp 跳出循环指令的地址,回填。
初始化清零。
*/

int address[100];
for (int j = 0; j < 100; j++){
address[j] = 0;
}

/*
找到 k 的位置准备放入栈顶
*/


do
{

int temp = cx2;
while (temp < cx3){
code[cx] = code[temp];
temp++;
cx++;
}
/*
读入 case 后面的表达式
如果是 otherwise 跳出循环
*/

getsym();
if (sym == otherwisesym) {
break;
}

/*
计算表达式的值
*/

simpleexpression(set_of_enum(symbol)::of(colon, eos) + fsys, x, level);

/*
error(24) y应为冒号
*/

if (sym != colon){
error(24);
}
getsym();

/*
比较二者, 相等时执行该对应的程序,
不相等时到下一个 case 再判断。
*/

gen(eq, 0, 0);

/*
记录 jpc 跳转指令地址,后面回填
*/

cx1 = cx;

/*
不相等时跳转,地址未知。
*/

gen(jpc, 0, 0);

/*
如果相等,执行后面的语句块
*/

statement(fsys, level);

/*
记录 jmp 跳转指令的地址,回填
执行完语句块跳出 case 语句
jmp无条件跳转,但是地址未知。
by yz_w
*/

address[case_num++] = cx;
gen(jmp, 0, 0);

/*
回填 jpc 挑战地址
*/

code[cx1].a = cx;
}
/*
只要读到 分号 , 继续读下一个 case
最后一条语句没有 分号
*/

while (sym == semicolon);

/*
otherwise 语句执行
如果有 otherwise
再读到 : colon
执行 otherwise 后面的语句块
*/

if (sym == otherwisesym) {
getsym();
if (sym != colon) {
error(24);
}
getsym();
statement(fsys, level);
}

/*
sym = end 表示 case 语句执行完毕
end 后面有分号, 但不必读入分号作为判断。
原因是分号是做为整个 case 块的结束标志,同时也是是否存在下一块的标识
若有分号,说明 case 下面还有程序块,
因此分号的判断不在这里,而在 casestatement() 返回后,详见 compound()函数
*/

if (sym != endsym) {
error(36);
}

for (int j = 0; j < case_num; j++)
code[address[j]].a = cx;
getsym();
}

4. goto 跳转

(1)准备工作

  首先新建一些全局变量来实现 goto语句。

//声明的 label的数目。 
int labeltotal = 0;

//程序中调用 goto 的数目
int gotototal = 0;

//存放 label 的标识, 最多存放 10 个,每声明一个label 就顺序在
//labelnum[] 中存放相应的 label 值。
int labelnum[10];

//存放 label 跳转地址,假设声明 label:a 那么程序中 goto a; 的跳
//转地址为 labelcx[a]。在程序中中每出现一个 label 标识就记录它
//的当前地址赋值给 labelcx[]。goto 语句跳转时会查询 labelcx[]
//数组确认它的跳转地址。
int labelcx[10000];

// gotonum[i][0]存放 goto 语句对应的 label 值,gotonum[i][1]记
//录相应的需要跳转的地址,当然跳转地址不知道,必须要在所有代码
//扫描完毕后再回填跳转地址。
int gotonum[10][2]

//某个 label 可能声明了,但是并没有出现。如果 goto 跳转到那个
//label 应当报错,建立 labelbool[] 判断 label 是否在程序中出现,
//出现为 1, 否为 0。
int labelbool[10000];

  由于 pascal 语言中 goto 语句的label必须是四位整数,因此这里数组只开到 10000,对于label的数量也做了限制,最大为10个,相应数组也只开到了10。
  首先添加 labelsym 和 gotosym 到 symbal{} 中去,并在initial()函数中的declbegsys{}集合中添加labelsym,在statbegsys{}中添加 labelsym 、gotosym、intcon。因为程序中label 的出现是直接出现数字,如例子中的‘10:’,所以把 intcon 也要添加到 statbegsys{}中去。
  最重要的一点是,goto 语句出现时可能label语句并没有出现,像上面的例子一样,所以不知道跳转的地址,因此goto跳转时地址需要回填。
  num ,是原有程序中的一个全局变量,用于保存最近一次读入的数组。

(2)代码思路

goto循环例子:

PL0编译器分析与语法扩展
(1)首先记下 a + c 表达式计算前的地址 cx1 = cx,考虑到k有可能是表达式然后计算 k 的值,再记下执行后的地址 cx2 = cx
(2)新建变量case_num = 0; 表示 case 语句情况的个数,每有一个 case 就执行 case_num++,
(3)每遇到一个 case 语句都要进行 k 值与 case 值的判断,判断前把 code[]数组中从cx2至cx3 的代码复制过来,即每遇到一个 case 都会执行一遍 cx2 至 cx3 的代码,即计算 k 的值。
(4)每个 case 语句执行完的地址都是相同的,都是用 jmp 跳出 case 语句,但是并不知道地址,用 address[100] 存放各 case 语句的 jmp 指令的地址,读到 end 后用 cx 回填 address[] 中存放的地址对应的 jmp 跳转地址。
(5)读到 otherwise 说明case值没有匹配上,执行otherwise后面的程序,直接调用 statement()。
(6)读到 end 表示结束。

2. 代码

(1)变量初始化

在 main() 函数中添加(block() 之前)
PL0编译器分析与语法扩展

(2)在block()函数的 do_while循环中增加对labelsym的识别

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

(3)在 Statement() 函数中增加对label的识别。

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

(4)在 Statement() 函数中增加对goto的识别。

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

(5)回填 jmp 指令地址

  因为回填必须在编译完所有代码后,所以在main()函数中调用完block()后开始回填
  PL0编译器分析与语法扩展

5. += 、-=

(1)准备工作

  += 和-=实现上是完全类似的,本质上都是赋值语句,因此我们修改 assisnmen() 函数。
  首先新建关键字 addsym 标识 +=, subsym 标识 -=, 并添加到 symbal中,但这里不添加到 initial() 函数中去,因为 += 和 -= 不是单词符号,且是两个字符连在一起,所以不能添加到 word1[] 或者是 ssym[]中去。

(2)代码思路

  对 += 和 -= 的识别应该在 getsym() 函数中,原来是读入+ ,sym =plusym,这里修改为读入 + 或 - 要判断后面的字符是否为 = 。因为我们还添加了 ++ 和 – 操作,同样在这里处理,我们一并修改。
  识别后赋值操作在assignmen() 函数中实现,添加对 addsym 和 subsym 的识别和相应的赋值处理。

对于 a += b; 的处理思路为:
(1)使用指令 loda 把该变量a的地址压入栈
(2)使用指令 lod 把变量a的值压入栈
(3)使用expression()函数计算 b 的值计算并压栈。
(4)执行 add 指令把两值相加(减),即弹两个栈再压一个栈。
(5)执行 sto 指令把得到的值存入变量地址中(赋值),即弹两个栈但不再压栈

2. 代码

(1)+= 和 -= 的识别,在getsym() 函数中找到识别 + 和 - 的位置

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

(2)赋值操作,修改assignment()函数,增加对 addsym 和 subsym 的识别。

+=运算
PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

-= 运算,把add指令换成sub指令就好
PL0编译器分析与语法扩展

6. ++ 、–运算

(1)准备工作

  在 symbal 中新建 ++ 关键字 addaddsym 和 – 关键字 subsubsym。同样也不对 initial() 做修改。

(2)代码思路

  同样的,++ 和–是类似的,只讨论一种。
++ 运算分为两种,一种是作为表达式的因子, 一种则是作为单独的语句。比如:
++a * b / 3;
a++ * b / 3;
++ 运算符作为因子

++a;
a++;
++ 运算符作为完整表达式
  
  虽然计算方式一样,但是编译程序编译就完全不同了,单独表达式的识别应该在 statement() 函数中实现,而因子处理应该在 factor() 函数中实现。同样的 – 也是如此。
  再往下分类,++a 和 a++ 也是不同的,作为单独表达式时,a++ 的处理应该是先识别变量a, 然后在 assignment() 中实现加法; 而 ++a 先识别 ++ 在运行加法。 而因子处理时factor()函数对于 a++ 也是先识别变量a,然后计算,而++a 先识别 ++。
所以对于 ++ 有四种不同的方式,– 也类似。

2. 代码

(一)++ 作为单独表达式在变量后,如 a++;

修改 assingnment()函数,当读到 ++ 后执行,注意要返回。
PL0编译器分析与语法扩展
– 类似
PL0编译器分析与语法扩展

(二)++ 作为单独表达式在变量前,如 ++a;

修改statement() 函数,当读入 ++ 执行。
PL0编译器分析与语法扩展
– 类似
PL0编译器分析与语法扩展

(三)++ 作为因子表达式在变量后,如 a++ * b + 3;

修改 factor() 函数。factor()用于计算表达式中的因子,如 a++、b、3。
在 factor() 中找到读入变量 a的代码 case ident:
首先先注释一句:
PL0编译器分析与语法扩展
  在 case ident: 块中还有个 switch 用于判断是常量还是变量,++只使用变量,我们在 case variable:的块的最后添加代码:
  最重要的是,例子中计算的是 a + 1 前的值,按照一般顺序会计算成 a + 1 后的值,所以我们压入两个a值,正常计算后,还有一个a值未被弹出,因此计算时使用栈顶的值,是未 + 1前的a值。
  因为在 case variable:代码里已压入a值,这里再压入一个a值,后续加法赋值与前面相同。
PL0编译器分析与语法扩展
– 类似, 因为 getsym() 在 上图 addaddsym 前已使用,这里不再调用
PL0编译器分析与语法扩展

(四)++ 作为因子表达式在变量前,如 ++a * b + 3;

  同样在 factor() 函数中增加读到 ++ 后的操作,与上面的 case ident;是并列的。同样先判断类型等,但不同的是最后增加压栈操作,因为 sto 赋值指令不会把赋值后的值压栈,但是表达式的计算需要用到a + 1后的值,所以在压一次栈。下图中的后三行代码。
PL0编译器分析与语法扩展

PL0编译器分析与语法扩展
– 类似
PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

7. 注释

(1)准备工作

  注释跟 c++ 类似,{}用于单行注释,{….}用于多行注释,思路是直接修改 getsym() 函数,一旦getch() 读到{ } 就判断是不是注释功能。

(2)代码实现

在 getsym() 中添加:

if (ch == '{'){
getch();
if (ch == '*'){
while (1){
if (ch == '.'){
error(101);
break;
}
if (ch == '*'){
getch();
if (ch == '}'){
break;
}
continue;
}
getch();
}
}
else{
while (1)
{
if (ch == '.'){
error(105);
break;
}
if (ch == ';'){
getch();
while (ch == ' '){
getch();
}
if (ch != '}'){
error(105);
break;
}
break;
}
getch();
}
}
getch();
getsym();
}
}

四、测试样例

(1)for 循环

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

(2)Repeat循环

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

(3)Case 判断

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

(4)Goto 跳转

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

(5)+= 和 -=

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

(6)++ 和 –

单独表达式:
PL0编译器分析与语法扩展

PL0编译器分析与语法扩展
++ – 作为因子处理
PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

(7)注释

PL0编译器分析与语法扩展

PL0编译器分析与语法扩展

转载请注明来自 b0t0w1’blog