声明
本次实验是对pascal语言的子集PL\0语言进行词法、语法分析、语义分析、中间代码生成和解释器执行中间代码等五个部分
实验采用python编写,优点在于列表和字典数据结构以及简洁的语法使得较少的代码量实现整个功能,如果你采用别的语言也可以参考本教程,我会尽量详细的说明实验的细节
理论和实验参考了以下博客:
http://jcf94.com/2016/02/21/2016-02-21-pl0/ 这个和南航的实验有些差别,但包括了主要的实验思路
https://www.cnblogs.com/X-Jun/p/11042544.html#_lab2_2_0 这个详细介绍了符号表和运行时存储组织记录
https://www.jianshu.com/p/de9132228b99 这个介绍了中间代码的规则和地址回填的方法
实验要求
课程设计题目
一个PASCAL语言子集(PL/0)编译器的设计与实现
PL/0语言的BNF描述(扩充的巴克斯范式表示法)
<prog> → program <id>;<block>
<block> → [<condecl>][<vardecl>][<proc>]<body>
<condecl> → const <const>{,<const>};
<const> → <id>:=<integer>
<vardecl> → var <id>{,<id>};
<proc> → procedure <id>([<id>{,<id>}]);<block>{;<proc>}
<body> → begin <statement>{;<statement>}end
<statement> → <id> := <exp>
|if <lexp> then <statement>[else <statement>]
|while <lexp> do <statement>
|call <id>([<exp>{,<exp>}])
|<body>
|read (<id>{,<id>})
|write (<exp>{,<exp>})
<lexp> → <exp> <lop> <exp>|odd <exp>
<exp> → [+|-]<term>{<aop><term>}
<term> → <factor>{<mop><factor>}
<factor>→<id>|<integer>|(<exp>)
<lop> → =|<>|<|<=|>|>=
<aop> → +|-
<mop> → *|/
<id> → l{l|d} (注:l表示字母)
<integer> → d{d}
注释:
<prog>:程序 ;<block>:块、程序体 ;<condecl>:常量说明 ;<const>:常量;<vardecl>:变量说明 ;<proc>:分程
序 ; <body>:复合语句 ;<statement>:语句;<exp>:表达式 ;<lexp>:条件 ;<term>:项 ; <factor>:因子 ;
<aop>:加法运算符;<mop>:乘法运算符; <lop>:关系运算符。
红色字体为PL\0语言的保留字,| 符号表示候选项只能多个选项选一个,{}表示可以出现多次
中间代码说明
Pcode的指令格式为
F :操作码
L :层次差 (标识符引用层减去定义层)
A :不同的指令含义不同
假想目标机的代码
LIT 0 ,a 取常量a放入数据栈栈顶
OPR 0 ,a 执行运算,a表示执行某种运算
LOD L ,a 取变量(相对地址为a,层差为L)放到数据栈的栈顶
STO L ,a 将数据栈栈顶的内容存入变量(相对地址为a,层次差为L)
CAL L ,a 调用过程(转子指令)(入口地址为a,层次差为L)
INT 0 ,a 数据栈栈顶指针增加a
JMP 0 ,a无条件转移到地址为a的指令
JPC 0 ,a 条件转移指令,转移到地址为a的指令
RED L ,a 读数据并存入变量(相对地址为a,层次差为L)
WRT 0 ,0 将栈顶内容输出
指令 | 具体含义 |
---|---|
LIT 0, a | 取常量a放到数据栈栈顶 |
OPR 0, a | 执行运算,a表示执行何种运算(+ - * /) |
LOD l, a | 取变量放到数据栈栈顶(相对地址为a,层次差为l) |
STO l, a | 将数据栈栈顶内容存入变量(相对地址为a,层次差为l) |
CAL l, a | 调用过程(入口指令地址为a,层次差为l) |
INT 0, a | 数据栈栈顶指针增加a |
JMP 0, a | 无条件转移到指令地址a |
JPC 0, a | 条件转移到指令地址a |
OPR 0 0 | 过程调用结束后,返回调用点并退栈 |
OPR 0 1 | 栈顶元素取反 |
OPR 0 2 | 次栈顶与栈顶相加,退两个栈元素,结果值进栈 |
OPR 0 3 | 次栈顶减去栈顶,退两个栈元素,结果值进栈 |
OPR 0 4 | 次栈顶乘以栈顶,退两个栈元素,结果值进栈 |
OPR 0 5 | 次栈顶除以栈顶,退两个栈元素,结果值进栈 |
OPR 0 6 | 栈顶元素的奇偶判断,结果值在栈顶 |
OPR 0 7 | |
OPR 0 8 | 次栈顶与栈顶是否相等,退两个栈元素,结果值进栈 |
OPR 0 9 | 次栈顶与栈顶是否不等,退两个栈元素,结果值进栈 |
OPR 0 10 | 次栈顶是否小于栈顶,退两个栈元素,结果值进栈 |
OPR 0 11 | 次栈顶是否大于等于栈顶,退两个栈元素,结果值进栈 |
OPR 0 12 | 次栈顶是否大于栈顶,退两个栈元素,结果值进栈 |
OPR 0 13 | 次栈顶是否小于等于栈顶,退两个栈元素,结果值进栈 |
OPR 0 14 | 栈顶值输出至屏幕 |
OPR 0 15 | 屏幕输出换行 |
OPR 0 16 | 从命令行读入一个输入置于栈顶 |
进一步说明:
INT:为被调用的过程(包括主过程)在运行栈S中开辟数据区,这时A段为所需数据单元个数(包括三个连接数据);L段恒为0。
CAL:调用过程,这时A段为被调用过程的过程体(过程体之前一条指令)在目标程序区的入口地址。
LIT:将常量送到运行栈S的栈顶,这时A段为常量值。
LOD:将变量送到运行栈S的栈顶,这时A段为变量所在说明层中的相对位置。
STO:将运行栈S的栈顶内容送入某个变量单元中,A段为变量所在说明层中的相对位置。
JMP:无条件转移,这时A段为转向地址(目标程序)。
JPC:条件转移,当运行栈S的栈顶的布尔值为假(0)时,则转向A段所指目标程序地址;否则顺序执行。
OPR:关系或算术运算,A段指明具体运算,例如A=2代表算术运算“+”;A=12代表关系运算“>”;A=16代表“读入”操作等等。运算对象取自运行栈S的栈顶及次栈顶。
活动记录说明
假想机结构
两个存储器:存储器CODE,用来存放P的代码
数据存储器STACK(栈)用来动态分配数据空间
四个寄存器:
一个指令寄存器I:存放当前要执行的代码
一个栈顶指示器寄存器T:指向数据栈STACK的栈顶
一个基地址寄存器B:存放当前运行过程的数据区在STACK中的起始地址
一个程序地址寄存器P:存放下一条要执行的指令地址
该假想机没有供运算用的寄存器。所有运算都要在数据栈STACK的栈顶两个单元之间进行,并用运算结果取代原来的两个运算对象而保留在栈顶。
活动记录
RA:返回地址
DL:调用者的活动记录首地址
SL:保存该过程直接外层的活动记录首地址
过程返回可以看成是执行一个特殊的OPR运算
注意:层次差为调用层次与定义层次的差值